From 408c7ca142689136d887676def1bf41ea80bb2a9 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 8 Aug 2025 16:09:39 -0700 Subject: [PATCH] chore: remove the TypeScript code from the repository (#2048) This deletes the bulk of the `codex-cli` folder and eliminates the logic that builds the TypeScript code and bundles it into the release. Since this PR modifies `.github/workflows/rust-release.yml`, to test changes to the release process, I locally commented out all of the "is this commit on upstream `main`" checks in `scripts/create_github_release.sh` and ran: ``` ./codex-rs/scripts/create_github_release.sh 0.20.0-alpha.4 ``` Which kicked off: https://github.com/openai/codex/actions/runs/16842085113 And the release artifacts appear legit! https://github.com/openai/codex/releases/tag/rust-v0.20.0-alpha.4 --- .github/workflows/ci.yml | 27 +- .github/workflows/codex.yml | 31 - .github/workflows/rust-release.yml | 26 - .husky/pre-commit | 1 - codex-cli/.editorconfig | 9 - codex-cli/.eslintrc.cjs | 107 - codex-cli/HUSKY.md | 45 - codex-cli/build.mjs | 88 - codex-cli/default.nix | 43 - codex-cli/examples/README.md | 44 - codex-cli/examples/build-codex-demo/run.sh | 65 - .../examples/build-codex-demo/runs/.gitkeep | 0 codex-cli/examples/build-codex-demo/task.yaml | 88 - codex-cli/examples/camerascii/run.sh | 68 - codex-cli/examples/camerascii/runs/.gitkeep | 0 codex-cli/examples/camerascii/task.yaml | 5 - .../camerascii/template/screenshot_details.md | 34 - codex-cli/examples/impossible-pong/run.sh | 68 - .../examples/impossible-pong/runs/.gitkeep | 0 codex-cli/examples/impossible-pong/task.yaml | 11 - .../impossible-pong/template/index.html | 233 - codex-cli/examples/prompt-analyzer/run.sh | 68 - .../examples/prompt-analyzer/runs/.gitkeep | 0 codex-cli/examples/prompt-analyzer/task.yaml | 17 - .../prompt-analyzer/template/Clustering.ipynb | 231 - .../prompt-analyzer/template/README.md | 103 - .../prompt-analyzer/template/analysis.md | 23 - .../template/analysis_dbscan.md | 22 - .../template/cluster_prompts.py | 547 -- .../template/plots/cluster_sizes.png | Bin 19000 -> 0 bytes .../prompt-analyzer/template/plots/tsne.png | Bin 102093 -> 0 bytes .../template/plots_dbscan/cluster_sizes.png | Bin 20441 -> 0 bytes .../template/plots_dbscan/tsne.png | Bin 96389 -> 0 bytes .../prompt-analyzer/template/prompts.csv | 214 - codex-cli/examples/prompting_guide.md | 117 - codex-cli/ignore-react-devtools-plugin.js | 16 - codex-cli/package.json | 71 +- codex-cli/require-shim.js | 11 - codex-cli/scripts/stage_release.sh | 8 - codex-cli/src/app.tsx | 108 - codex-cli/src/approvals.ts | 633 -- codex-cli/src/cli-singlepass.tsx | 28 - codex-cli/src/cli.tsx | 740 --- .../src/components/approval-mode-overlay.tsx | 47 - .../src/components/chat/message-history.tsx | 86 - .../src/components/chat/multiline-editor.tsx | 392 -- .../chat/terminal-chat-command-review.tsx | 256 - .../chat/terminal-chat-completions.tsx | 64 - .../chat/terminal-chat-input-thinking.tsx | 129 - .../components/chat/terminal-chat-input.tsx | 1017 ---- .../chat/terminal-chat-past-rollout.tsx | 68 - .../chat/terminal-chat-response-item.tsx | 360 -- .../chat/terminal-chat-tool-call-command.tsx | 143 - .../src/components/chat/terminal-chat.tsx | 766 --- .../src/components/chat/terminal-header.tsx | 99 - .../chat/terminal-message-history.tsx | 93 - .../components/chat/use-message-grouping.ts | 9 - codex-cli/src/components/diff-overlay.tsx | 93 - codex-cli/src/components/help-overlay.tsx | 103 - codex-cli/src/components/history-overlay.tsx | 255 - codex-cli/src/components/model-overlay.tsx | 165 - .../onboarding/onboarding-approval-mode.tsx | 35 - .../src/components/select-input/indicator.tsx | 21 - .../src/components/select-input/item.tsx | 13 - .../components/select-input/select-input.tsx | 189 - codex-cli/src/components/sessions-overlay.tsx | 130 - .../src/components/singlepass-cli-app.tsx | 677 --- .../src/components/typeahead-overlay.tsx | 166 - .../components/vendor/cli-spinners/index.js | 1293 ---- .../src/components/vendor/ink-select/index.js | 1 - .../vendor/ink-select/option-map.js | 26 - .../vendor/ink-select/select-option.js | 27 - .../components/vendor/ink-select/select.js | 53 - .../src/components/vendor/ink-select/theme.js | 32 - .../vendor/ink-select/use-select-state.js | 158 - .../vendor/ink-select/use-select.js | 17 - .../src/components/vendor/ink-spinner.tsx | 36 - .../src/components/vendor/ink-text-input.tsx | 428 -- codex-cli/src/format-command.ts | 53 - codex-cli/src/hooks/use-confirmation.ts | 67 - codex-cli/src/hooks/use-terminal-size.ts | 26 - codex-cli/src/parse-apply-patch.ts | 113 - codex-cli/src/shims-external.d.ts | 24 - codex-cli/src/text-buffer.ts | 977 --- codex-cli/src/typings.d.ts | 65 - codex-cli/src/utils/agent/agent-loop.ts | 1674 ----- codex-cli/src/utils/agent/apply-patch.ts | 815 --- codex-cli/src/utils/agent/exec.ts | 138 - .../src/utils/agent/handle-exec-command.ts | 377 -- .../src/utils/agent/parse-apply-patch.ts | 112 - .../src/utils/agent/platform-commands.ts | 82 - codex-cli/src/utils/agent/review.ts | 14 - .../sandbox/create-truncating-collector.ts | 77 - .../src/utils/agent/sandbox/interface.ts | 30 - codex-cli/src/utils/agent/sandbox/landlock.ts | 175 - .../src/utils/agent/sandbox/macos-seatbelt.ts | 154 - codex-cli/src/utils/agent/sandbox/raw-exec.ts | 238 - .../src/utils/approximate-tokens-used.ts | 55 - codex-cli/src/utils/auto-approval-mode.js | 9 - codex-cli/src/utils/auto-approval-mode.ts | 10 - codex-cli/src/utils/bug-report.ts | 82 - codex-cli/src/utils/check-in-git.ts | 31 - codex-cli/src/utils/check-updates.ts | 146 - codex-cli/src/utils/compact-summary.ts | 70 - codex-cli/src/utils/config.ts | 597 -- .../src/utils/extract-applied-patches.ts | 36 - .../src/utils/file-system-suggestions.ts | 59 - codex-cli/src/utils/file-tag-utils.ts | 62 - .../src/utils/get-api-key-components.tsx | 75 - codex-cli/src/utils/get-api-key.tsx | 766 --- codex-cli/src/utils/get-diff.ts | 129 - codex-cli/src/utils/input-utils.ts | 39 - codex-cli/src/utils/logger/log.ts | 137 - codex-cli/src/utils/model-info.ts | 202 - codex-cli/src/utils/model-utils.ts | 196 - codex-cli/src/utils/openai-client.ts | 51 - .../src/utils/package-manager-detector.ts | 73 - codex-cli/src/utils/parsers.ts | 113 - codex-cli/src/utils/providers.ts | 55 - codex-cli/src/utils/responses.ts | 717 --- codex-cli/src/utils/session.ts | 52 - codex-cli/src/utils/short-path.ts | 27 - codex-cli/src/utils/singlepass/code_diff.ts | 190 - codex-cli/src/utils/singlepass/context.ts | 65 - .../src/utils/singlepass/context_files.ts | 409 -- .../src/utils/singlepass/context_limit.ts | 208 - codex-cli/src/utils/singlepass/file_ops.ts | 47 - codex-cli/src/utils/slash-commands.ts | 36 - .../src/utils/storage/command-history.ts | 139 - codex-cli/src/utils/storage/save-rollout.ts | 52 - codex-cli/src/utils/terminal-chat-utils.ts | 0 codex-cli/src/utils/terminal.ts | 84 - codex-cli/src/version.ts | 8 - codex-cli/tests/__fixtures__/a.txt | 1 - codex-cli/tests/__fixtures__/b.txt | 1 - .../__snapshots__/check-updates.test.ts.snap | 12 - .../agent-azure-responses-endpoint.test.ts | 107 - codex-cli/tests/agent-cancel-early.test.ts | 128 - .../tests/agent-cancel-prev-response.test.ts | 149 - codex-cli/tests/agent-cancel-race.test.ts | 137 - codex-cli/tests/agent-cancel.test.ts | 171 - codex-cli/tests/agent-dedupe-items.test.ts | 115 - .../tests/agent-function-call-id.test.ts | 150 - .../tests/agent-generic-network-error.test.ts | 134 - .../tests/agent-interrupt-continue.test.ts | 148 - .../tests/agent-invalid-request-error.test.ts | 89 - .../tests/agent-max-tokens-error.test.ts | 93 - codex-cli/tests/agent-network-errors.test.ts | 181 - codex-cli/tests/agent-project-doc.test.ts | 142 - .../tests/agent-rate-limit-error.test.ts | 128 - codex-cli/tests/agent-server-retry.test.ts | 168 - codex-cli/tests/agent-terminate.test.ts | 180 - codex-cli/tests/agent-thinking-time.test.ts | 174 - codex-cli/tests/api-key.test.ts | 35 - codex-cli/tests/apply-patch.test.ts | 346 -- codex-cli/tests/approvals.test.ts | 219 - codex-cli/tests/cancel-exec.test.ts | 56 - codex-cli/tests/check-updates.test.ts | 178 - codex-cli/tests/clear-command.test.tsx | 120 - codex-cli/tests/config.test.tsx | 363 -- codex-cli/tests/config_reasoning.test.ts | 121 - .../tests/create-truncating-collector.test.ts | 55 - .../disableResponseStorage.agentLoop.test.ts | 93 - .../tests/disableResponseStorage.test.ts | 46 - codex-cli/tests/dummy.test.ts | 4 - codex-cli/tests/exec-apply-patch.test.ts | 44 - .../tests/file-system-suggestions.test.ts | 92 - codex-cli/tests/file-tag-utils.test.ts | 240 - codex-cli/tests/fixed-requires-shell.test.ts | 38 - codex-cli/tests/format-command.test.ts | 21 - .../tests/get-diff-special-chars.test.ts | 28 - codex-cli/tests/history-overlay.test.tsx | 350 -- codex-cli/tests/input-utils.test.ts | 47 - .../tests/invalid-command-handling.test.ts | 68 - codex-cli/tests/markdown.test.tsx | 172 - codex-cli/tests/model-info.test.ts | 19 - .../tests/model-utils-network-error.test.ts | 76 - codex-cli/tests/model-utils.test.ts | 78 - .../multiline-ctrl-enter-submit.test.tsx | 41 - .../tests/multiline-dynamic-width.test.tsx | 77 - .../tests/multiline-enter-submit-cr.test.tsx | 41 - .../tests/multiline-history-behavior.test.tsx | 180 - codex-cli/tests/multiline-input-test.ts | 164 - codex-cli/tests/multiline-newline.test.tsx | 56 - .../tests/multiline-shift-enter-crlf.test.tsx | 51 - .../tests/multiline-shift-enter-mod1.test.tsx | 49 - .../tests/multiline-shift-enter.test.tsx | 49 - .../tests/package-manager-detector.test.ts | 66 - codex-cli/tests/parse-apply-patch.test.ts | 45 - codex-cli/tests/pipe-command.test.ts | 19 - codex-cli/tests/project-doc.test.ts | 57 - .../tests/raw-exec-process-group.test.ts | 87 - codex-cli/tests/requires-shell.test.ts | 49 - .../tests/responses-chat-completions.test.ts | 815 --- codex-cli/tests/slash-commands.test.ts | 40 - .../tests/terminal-chat-completions.test.tsx | 46 - .../terminal-chat-input-compact.test.tsx | 34 - ...l-chat-input-file-tag-suggestions.test.tsx | 207 - .../terminal-chat-input-multiline.test.tsx | 130 - .../terminal-chat-model-selection.test.tsx | 130 - .../terminal-chat-response-item.test.tsx | 65 - .../tests/text-buffer-copy-paste.test.ts | 50 - codex-cli/tests/text-buffer-crlf.test.ts | 14 - codex-cli/tests/text-buffer-gaps.test.ts | 250 - codex-cli/tests/text-buffer-word.test.ts | 137 - codex-cli/tests/text-buffer.test.ts | 291 - .../tests/token-streaming-performance.test.ts | 110 - codex-cli/tests/typeahead-scroll.test.tsx | 68 - codex-cli/tests/ui-test-helpers.tsx | 28 - codex-cli/tests/user-config-env.test.ts | 62 - codex-cli/tsconfig.json | 34 - codex-cli/vitest.config.ts | 12 - package.json | 29 +- patches/marked-terminal@7.3.0.patch | 26 - pnpm-lock.yaml | 5397 ----------------- pnpm-workspace.yaml | 5 - 216 files changed, 3 insertions(+), 35960 deletions(-) delete mode 100644 .husky/pre-commit delete mode 100644 codex-cli/.editorconfig delete mode 100644 codex-cli/.eslintrc.cjs delete mode 100644 codex-cli/HUSKY.md delete mode 100644 codex-cli/build.mjs delete mode 100644 codex-cli/default.nix delete mode 100644 codex-cli/examples/README.md delete mode 100755 codex-cli/examples/build-codex-demo/run.sh delete mode 100644 codex-cli/examples/build-codex-demo/runs/.gitkeep delete mode 100644 codex-cli/examples/build-codex-demo/task.yaml delete mode 100755 codex-cli/examples/camerascii/run.sh delete mode 100644 codex-cli/examples/camerascii/runs/.gitkeep delete mode 100644 codex-cli/examples/camerascii/task.yaml delete mode 100644 codex-cli/examples/camerascii/template/screenshot_details.md delete mode 100755 codex-cli/examples/impossible-pong/run.sh delete mode 100644 codex-cli/examples/impossible-pong/runs/.gitkeep delete mode 100644 codex-cli/examples/impossible-pong/task.yaml delete mode 100644 codex-cli/examples/impossible-pong/template/index.html delete mode 100755 codex-cli/examples/prompt-analyzer/run.sh delete mode 100644 codex-cli/examples/prompt-analyzer/runs/.gitkeep delete mode 100644 codex-cli/examples/prompt-analyzer/task.yaml delete mode 100644 codex-cli/examples/prompt-analyzer/template/Clustering.ipynb delete mode 100644 codex-cli/examples/prompt-analyzer/template/README.md delete mode 100644 codex-cli/examples/prompt-analyzer/template/analysis.md delete mode 100644 codex-cli/examples/prompt-analyzer/template/analysis_dbscan.md delete mode 100644 codex-cli/examples/prompt-analyzer/template/cluster_prompts.py delete mode 100644 codex-cli/examples/prompt-analyzer/template/plots/cluster_sizes.png delete mode 100644 codex-cli/examples/prompt-analyzer/template/plots/tsne.png delete mode 100644 codex-cli/examples/prompt-analyzer/template/plots_dbscan/cluster_sizes.png delete mode 100644 codex-cli/examples/prompt-analyzer/template/plots_dbscan/tsne.png delete mode 100644 codex-cli/examples/prompt-analyzer/template/prompts.csv delete mode 100644 codex-cli/examples/prompting_guide.md delete mode 100644 codex-cli/ignore-react-devtools-plugin.js delete mode 100644 codex-cli/require-shim.js delete mode 100644 codex-cli/src/app.tsx delete mode 100644 codex-cli/src/approvals.ts delete mode 100644 codex-cli/src/cli-singlepass.tsx delete mode 100644 codex-cli/src/cli.tsx delete mode 100644 codex-cli/src/components/approval-mode-overlay.tsx delete mode 100644 codex-cli/src/components/chat/message-history.tsx delete mode 100644 codex-cli/src/components/chat/multiline-editor.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-command-review.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-completions.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-input-thinking.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-input.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-past-rollout.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-response-item.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat.tsx delete mode 100644 codex-cli/src/components/chat/terminal-header.tsx delete mode 100644 codex-cli/src/components/chat/terminal-message-history.tsx delete mode 100644 codex-cli/src/components/chat/use-message-grouping.ts delete mode 100644 codex-cli/src/components/diff-overlay.tsx delete mode 100644 codex-cli/src/components/help-overlay.tsx delete mode 100644 codex-cli/src/components/history-overlay.tsx delete mode 100644 codex-cli/src/components/model-overlay.tsx delete mode 100644 codex-cli/src/components/onboarding/onboarding-approval-mode.tsx delete mode 100644 codex-cli/src/components/select-input/indicator.tsx delete mode 100644 codex-cli/src/components/select-input/item.tsx delete mode 100644 codex-cli/src/components/select-input/select-input.tsx delete mode 100644 codex-cli/src/components/sessions-overlay.tsx delete mode 100644 codex-cli/src/components/singlepass-cli-app.tsx delete mode 100644 codex-cli/src/components/typeahead-overlay.tsx delete mode 100644 codex-cli/src/components/vendor/cli-spinners/index.js delete mode 100644 codex-cli/src/components/vendor/ink-select/index.js delete mode 100644 codex-cli/src/components/vendor/ink-select/option-map.js delete mode 100644 codex-cli/src/components/vendor/ink-select/select-option.js delete mode 100644 codex-cli/src/components/vendor/ink-select/select.js delete mode 100644 codex-cli/src/components/vendor/ink-select/theme.js delete mode 100644 codex-cli/src/components/vendor/ink-select/use-select-state.js delete mode 100644 codex-cli/src/components/vendor/ink-select/use-select.js delete mode 100644 codex-cli/src/components/vendor/ink-spinner.tsx delete mode 100644 codex-cli/src/components/vendor/ink-text-input.tsx delete mode 100644 codex-cli/src/format-command.ts delete mode 100644 codex-cli/src/hooks/use-confirmation.ts delete mode 100644 codex-cli/src/hooks/use-terminal-size.ts delete mode 100644 codex-cli/src/parse-apply-patch.ts delete mode 100644 codex-cli/src/shims-external.d.ts delete mode 100644 codex-cli/src/text-buffer.ts delete mode 100644 codex-cli/src/typings.d.ts delete mode 100644 codex-cli/src/utils/agent/agent-loop.ts delete mode 100644 codex-cli/src/utils/agent/apply-patch.ts delete mode 100644 codex-cli/src/utils/agent/exec.ts delete mode 100644 codex-cli/src/utils/agent/handle-exec-command.ts delete mode 100644 codex-cli/src/utils/agent/parse-apply-patch.ts delete mode 100644 codex-cli/src/utils/agent/platform-commands.ts delete mode 100644 codex-cli/src/utils/agent/review.ts delete mode 100644 codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts delete mode 100644 codex-cli/src/utils/agent/sandbox/interface.ts delete mode 100644 codex-cli/src/utils/agent/sandbox/landlock.ts delete mode 100644 codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts delete mode 100644 codex-cli/src/utils/agent/sandbox/raw-exec.ts delete mode 100644 codex-cli/src/utils/approximate-tokens-used.ts delete mode 100644 codex-cli/src/utils/auto-approval-mode.js delete mode 100644 codex-cli/src/utils/auto-approval-mode.ts delete mode 100644 codex-cli/src/utils/bug-report.ts delete mode 100644 codex-cli/src/utils/check-in-git.ts delete mode 100644 codex-cli/src/utils/check-updates.ts delete mode 100644 codex-cli/src/utils/compact-summary.ts delete mode 100644 codex-cli/src/utils/config.ts delete mode 100644 codex-cli/src/utils/extract-applied-patches.ts delete mode 100644 codex-cli/src/utils/file-system-suggestions.ts delete mode 100644 codex-cli/src/utils/file-tag-utils.ts delete mode 100644 codex-cli/src/utils/get-api-key-components.tsx delete mode 100644 codex-cli/src/utils/get-api-key.tsx delete mode 100644 codex-cli/src/utils/get-diff.ts delete mode 100644 codex-cli/src/utils/input-utils.ts delete mode 100644 codex-cli/src/utils/logger/log.ts delete mode 100644 codex-cli/src/utils/model-info.ts delete mode 100644 codex-cli/src/utils/model-utils.ts delete mode 100644 codex-cli/src/utils/openai-client.ts delete mode 100644 codex-cli/src/utils/package-manager-detector.ts delete mode 100644 codex-cli/src/utils/parsers.ts delete mode 100644 codex-cli/src/utils/providers.ts delete mode 100644 codex-cli/src/utils/responses.ts delete mode 100644 codex-cli/src/utils/session.ts delete mode 100644 codex-cli/src/utils/short-path.ts delete mode 100644 codex-cli/src/utils/singlepass/code_diff.ts delete mode 100644 codex-cli/src/utils/singlepass/context.ts delete mode 100644 codex-cli/src/utils/singlepass/context_files.ts delete mode 100644 codex-cli/src/utils/singlepass/context_limit.ts delete mode 100644 codex-cli/src/utils/singlepass/file_ops.ts delete mode 100644 codex-cli/src/utils/slash-commands.ts delete mode 100644 codex-cli/src/utils/storage/command-history.ts delete mode 100644 codex-cli/src/utils/storage/save-rollout.ts delete mode 100644 codex-cli/src/utils/terminal-chat-utils.ts delete mode 100644 codex-cli/src/utils/terminal.ts delete mode 100644 codex-cli/src/version.ts delete mode 100644 codex-cli/tests/__fixtures__/a.txt delete mode 100644 codex-cli/tests/__fixtures__/b.txt delete mode 100644 codex-cli/tests/__snapshots__/check-updates.test.ts.snap delete mode 100644 codex-cli/tests/agent-azure-responses-endpoint.test.ts delete mode 100644 codex-cli/tests/agent-cancel-early.test.ts delete mode 100644 codex-cli/tests/agent-cancel-prev-response.test.ts delete mode 100644 codex-cli/tests/agent-cancel-race.test.ts delete mode 100644 codex-cli/tests/agent-cancel.test.ts delete mode 100644 codex-cli/tests/agent-dedupe-items.test.ts delete mode 100644 codex-cli/tests/agent-function-call-id.test.ts delete mode 100644 codex-cli/tests/agent-generic-network-error.test.ts delete mode 100644 codex-cli/tests/agent-interrupt-continue.test.ts delete mode 100644 codex-cli/tests/agent-invalid-request-error.test.ts delete mode 100644 codex-cli/tests/agent-max-tokens-error.test.ts delete mode 100644 codex-cli/tests/agent-network-errors.test.ts delete mode 100644 codex-cli/tests/agent-project-doc.test.ts delete mode 100644 codex-cli/tests/agent-rate-limit-error.test.ts delete mode 100644 codex-cli/tests/agent-server-retry.test.ts delete mode 100644 codex-cli/tests/agent-terminate.test.ts delete mode 100644 codex-cli/tests/agent-thinking-time.test.ts delete mode 100644 codex-cli/tests/api-key.test.ts delete mode 100644 codex-cli/tests/apply-patch.test.ts delete mode 100644 codex-cli/tests/approvals.test.ts delete mode 100644 codex-cli/tests/cancel-exec.test.ts delete mode 100644 codex-cli/tests/check-updates.test.ts delete mode 100644 codex-cli/tests/clear-command.test.tsx delete mode 100644 codex-cli/tests/config.test.tsx delete mode 100644 codex-cli/tests/config_reasoning.test.ts delete mode 100644 codex-cli/tests/create-truncating-collector.test.ts delete mode 100644 codex-cli/tests/disableResponseStorage.agentLoop.test.ts delete mode 100644 codex-cli/tests/disableResponseStorage.test.ts delete mode 100644 codex-cli/tests/dummy.test.ts delete mode 100644 codex-cli/tests/exec-apply-patch.test.ts delete mode 100644 codex-cli/tests/file-system-suggestions.test.ts delete mode 100644 codex-cli/tests/file-tag-utils.test.ts delete mode 100644 codex-cli/tests/fixed-requires-shell.test.ts delete mode 100644 codex-cli/tests/format-command.test.ts delete mode 100644 codex-cli/tests/get-diff-special-chars.test.ts delete mode 100644 codex-cli/tests/history-overlay.test.tsx delete mode 100644 codex-cli/tests/input-utils.test.ts delete mode 100644 codex-cli/tests/invalid-command-handling.test.ts delete mode 100644 codex-cli/tests/markdown.test.tsx delete mode 100644 codex-cli/tests/model-info.test.ts delete mode 100644 codex-cli/tests/model-utils-network-error.test.ts delete mode 100644 codex-cli/tests/model-utils.test.ts delete mode 100644 codex-cli/tests/multiline-ctrl-enter-submit.test.tsx delete mode 100644 codex-cli/tests/multiline-dynamic-width.test.tsx delete mode 100644 codex-cli/tests/multiline-enter-submit-cr.test.tsx delete mode 100644 codex-cli/tests/multiline-history-behavior.test.tsx delete mode 100644 codex-cli/tests/multiline-input-test.ts delete mode 100644 codex-cli/tests/multiline-newline.test.tsx delete mode 100644 codex-cli/tests/multiline-shift-enter-crlf.test.tsx delete mode 100644 codex-cli/tests/multiline-shift-enter-mod1.test.tsx delete mode 100644 codex-cli/tests/multiline-shift-enter.test.tsx delete mode 100644 codex-cli/tests/package-manager-detector.test.ts delete mode 100644 codex-cli/tests/parse-apply-patch.test.ts delete mode 100644 codex-cli/tests/pipe-command.test.ts delete mode 100644 codex-cli/tests/project-doc.test.ts delete mode 100644 codex-cli/tests/raw-exec-process-group.test.ts delete mode 100644 codex-cli/tests/requires-shell.test.ts delete mode 100644 codex-cli/tests/responses-chat-completions.test.ts delete mode 100644 codex-cli/tests/slash-commands.test.ts delete mode 100644 codex-cli/tests/terminal-chat-completions.test.tsx delete mode 100644 codex-cli/tests/terminal-chat-input-compact.test.tsx delete mode 100644 codex-cli/tests/terminal-chat-input-file-tag-suggestions.test.tsx delete mode 100644 codex-cli/tests/terminal-chat-input-multiline.test.tsx delete mode 100644 codex-cli/tests/terminal-chat-model-selection.test.tsx delete mode 100644 codex-cli/tests/terminal-chat-response-item.test.tsx delete mode 100644 codex-cli/tests/text-buffer-copy-paste.test.ts delete mode 100644 codex-cli/tests/text-buffer-crlf.test.ts delete mode 100644 codex-cli/tests/text-buffer-gaps.test.ts delete mode 100644 codex-cli/tests/text-buffer-word.test.ts delete mode 100644 codex-cli/tests/text-buffer.test.ts delete mode 100644 codex-cli/tests/token-streaming-performance.test.ts delete mode 100644 codex-cli/tests/typeahead-scroll.test.tsx delete mode 100644 codex-cli/tests/ui-test-helpers.tsx delete mode 100644 codex-cli/tests/user-config-env.test.ts delete mode 100644 codex-cli/tsconfig.json delete mode 100644 codex-cli/vitest.config.ts delete mode 100644 patches/marked-terminal@7.3.0.patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d8675fa5f..32fb070924 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,35 +44,10 @@ jobs: # 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 - run: pnpm run typecheck - - - name: Build - run: pnpm run build - - name: Ensure staging a release works. - working-directory: codex-cli env: GH_TOKEN: ${{ github.token }} - run: pnpm stage-release + run: ./codex-cli/scripts/stage_release.sh - name: Ensure root README.md contains only ASCII and certain Unicode code points run: ./scripts/asciicheck.py README.md diff --git a/.github/workflows/codex.yml b/.github/workflows/codex.yml index 18fe74cc85..6ca0d57f46 100644 --- a/.github/workflows/codex.yml +++ b/.github/workflows/codex.yml @@ -39,37 +39,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - # We install the dependencies like we would for an ordinary CI job, - # particularly because Codex will not have network access to install - # these dependencies. - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10.8.1 - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - 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 - - uses: dtolnay/rust-toolchain@1.88 with: targets: x86_64-unknown-linux-gnu diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index fc0938e20b..8e5aef2757 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -172,32 +172,6 @@ jobs: version="${GITHUB_REF_NAME#rust-v}" echo "name=${version}" >> $GITHUB_OUTPUT - # Setup Node + pnpm similar to ci.yml so we can build the npm package - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10.8.1 - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - 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: Stage npm package env: GH_TOKEN: ${{ github.token }} diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index e02c24e2b5..0000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -pnpm lint-staged \ No newline at end of file diff --git a/codex-cli/.editorconfig b/codex-cli/.editorconfig deleted file mode 100644 index f55d420563..0000000000 --- a/codex-cli/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 - -[*.{js,ts,jsx,tsx}] -indent_style = space -indent_size = 2 \ No newline at end of file diff --git a/codex-cli/.eslintrc.cjs b/codex-cli/.eslintrc.cjs deleted file mode 100644 index a623d2edb0..0000000000 --- a/codex-cli/.eslintrc.cjs +++ /dev/null @@ -1,107 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, node: true, es2020: true }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - ], - ignorePatterns: [ - ".eslintrc.cjs", - "build.mjs", - "dist", - "vite.config.ts", - "src/components/vendor", - ], - parser: "@typescript-eslint/parser", - parserOptions: { - tsconfigRootDir: __dirname, - project: ["./tsconfig.json"], - }, - plugins: ["import", "react-hooks", "react-refresh"], - rules: { - // Imports - "@typescript-eslint/consistent-type-imports": "error", - "import/no-cycle": ["error", { maxDepth: 1 }], - "import/no-duplicates": "error", - "import/order": [ - "error", - { - groups: ["type"], - "newlines-between": "always", - alphabetize: { - order: "asc", - caseInsensitive: false, - }, - }, - ], - // We use the import/ plugin instead. - "sort-imports": "off", - - "@typescript-eslint/array-type": ["error", { default: "generic" }], - // FIXME(mbolin): Introduce this. - // "@typescript-eslint/explicit-function-return-type": "error", - "@typescript-eslint/explicit-module-boundary-types": "error", - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/switch-exhaustiveness-check": [ - "error", - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: true, - }, - ], - - // Use typescript-eslint/no-unused-vars, no-unused-vars reports - // false positives with typescript - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - - curly: "error", - - eqeqeq: ["error", "always", { null: "never" }], - "react-refresh/only-export-components": [ - "error", - { allowConstantExport: true }, - ], - "no-await-in-loop": "error", - "no-bitwise": "error", - "no-caller": "error", - // This is fine during development, but should not be checked in. - "no-console": "error", - // This is fine during development, but should not be checked in. - "no-debugger": "error", - "no-duplicate-case": "error", - "no-eval": "error", - "no-ex-assign": "error", - "no-return-await": "error", - "no-param-reassign": "error", - "no-script-url": "error", - "no-self-compare": "error", - "no-unsafe-finally": "error", - "no-var": "error", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "error", - }, - overrides: [ - { - // apply only to files under tests/ - files: ["tests/**/*.{ts,tsx,js,jsx}"], - rules: { - "@typescript-eslint/no-explicit-any": "off", - "import/order": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-var-requires": "off", - "no-await-in-loop": "off", - "no-control-regex": "off", - }, - }, - ], -}; diff --git a/codex-cli/HUSKY.md b/codex-cli/HUSKY.md deleted file mode 100644 index d525e2f743..0000000000 --- a/codex-cli/HUSKY.md +++ /dev/null @@ -1,45 +0,0 @@ -# Husky Git Hooks - -This project uses [Husky](https://typicode.github.io/husky/) to enforce code quality checks before commits and pushes. - -## What's Included - -- **Pre-commit Hook**: Runs lint-staged to check files that are about to be committed. - - - Lints and formats TypeScript/TSX files using ESLint and Prettier - - Formats JSON, MD, and YML files using Prettier - -- **Pre-push Hook**: Runs tests and type checking before pushing to the remote repository. - - Executes `npm test` to run all tests - - Executes `npm run typecheck` to check TypeScript types - -## Benefits - -- Ensures consistent code style across the project -- Prevents pushing code with failing tests or type errors -- Reduces the need for style-related code review comments -- Improves overall code quality - -## For Contributors - -You don't need to do anything special to use these hooks. They will automatically run when you commit or push code. - -If you need to bypass the hooks in exceptional cases: - -```bash -# Skip pre-commit hooks -git commit -m "Your message" --no-verify - -# Skip pre-push hooks -git push --no-verify -``` - -Note: Please use these bypass options sparingly and only when absolutely necessary. - -## Troubleshooting - -If you encounter any issues with the hooks: - -1. Make sure you have the latest dependencies installed: `npm install` -2. Ensure the hook scripts are executable (Unix systems): `chmod +x .husky/pre-commit .husky/pre-push` -3. Check if there are any ESLint or Prettier configuration issues in your code diff --git a/codex-cli/build.mjs b/codex-cli/build.mjs deleted file mode 100644 index 16664d76fc..0000000000 --- a/codex-cli/build.mjs +++ /dev/null @@ -1,88 +0,0 @@ -import * as esbuild from "esbuild"; -import * as fs from "fs"; -import * as path from "path"; - -const OUT_DIR = 'dist' -/** - * ink attempts to import react-devtools-core in an ESM-unfriendly way: - * - * https://github.com/vadimdemedes/ink/blob/eab6ef07d4030606530d58d3d7be8079b4fb93bb/src/reconciler.ts#L22-L45 - * - * to make this work, we have to strip the import out of the build. - */ -const ignoreReactDevToolsPlugin = { - name: "ignore-react-devtools", - setup(build) { - // When an import for 'react-devtools-core' is encountered, - // return an empty module. - build.onResolve({ filter: /^react-devtools-core$/ }, (args) => { - return { path: args.path, namespace: "ignore-devtools" }; - }); - build.onLoad({ filter: /.*/, namespace: "ignore-devtools" }, () => { - return { contents: "", loader: "js" }; - }); - }, -}; - -// ---------------------------------------------------------------------------- -// Build mode detection (production vs development) -// -// • production (default): minified, external telemetry shebang handling. -// • development (--dev|NODE_ENV=development|CODEX_DEV=1): -// – no minification -// – inline source maps for better stacktraces -// – shebang tweaked to enable Node's source‑map support at runtime -// ---------------------------------------------------------------------------- - -const isDevBuild = - process.argv.includes("--dev") || - process.env.CODEX_DEV === "1" || - process.env.NODE_ENV === "development"; - -const plugins = [ignoreReactDevToolsPlugin]; - -// Build Hygiene, ensure we drop previous dist dir and any leftover files -const outPath = path.resolve(OUT_DIR); -if (fs.existsSync(outPath)) { - fs.rmSync(outPath, { recursive: true, force: true }); -} - -// Add a shebang that enables source‑map support for dev builds so that stack -// traces point to the original TypeScript lines without requiring callers to -// remember to set NODE_OPTIONS manually. -if (isDevBuild) { - const devShebangLine = - "#!/usr/bin/env -S NODE_OPTIONS=--enable-source-maps node\n"; - const devShebangPlugin = { - name: "dev-shebang", - setup(build) { - build.onEnd(async () => { - const outFile = path.resolve(isDevBuild ? `${OUT_DIR}/cli-dev.js` : `${OUT_DIR}/cli.js`); - let code = await fs.promises.readFile(outFile, "utf8"); - if (code.startsWith("#!")) { - code = code.replace(/^#!.*\n/, devShebangLine); - await fs.promises.writeFile(outFile, code, "utf8"); - } - }); - }, - }; - plugins.push(devShebangPlugin); -} - -esbuild - .build({ - entryPoints: ["src/cli.tsx"], - // Do not bundle the contents of package.json at build time: always read it - // at runtime. - external: ["../package.json"], - bundle: true, - format: "esm", - platform: "node", - tsconfig: "tsconfig.json", - outfile: isDevBuild ? `${OUT_DIR}/cli-dev.js` : `${OUT_DIR}/cli.js`, - minify: !isDevBuild, - sourcemap: isDevBuild ? "inline" : true, - plugins, - inject: ["./require-shim.js"], - }) - .catch(() => process.exit(1)); diff --git a/codex-cli/default.nix b/codex-cli/default.nix deleted file mode 100644 index 6ae19bb73a..0000000000 --- a/codex-cli/default.nix +++ /dev/null @@ -1,43 +0,0 @@ -{ pkgs, monorep-deps ? [], ... }: -let - node = pkgs.nodejs_22; -in -rec { - package = pkgs.buildNpmPackage { - pname = "codex-cli"; - version = "0.1.0"; - src = ./.; - npmDepsHash = "sha256-3tAalmh50I0fhhd7XreM+jvl0n4zcRhqygFNB1Olst8"; - nodejs = node; - npmInstallFlags = [ "--frozen-lockfile" ]; - meta = with pkgs.lib; { - description = "OpenAI Codex command‑line interface"; - license = licenses.asl20; - homepage = "https://github.com/openai/codex"; - }; - }; - devShell = pkgs.mkShell { - name = "codex-cli-dev"; - buildInputs = monorep-deps ++ [ - node - pkgs.pnpm - ]; - shellHook = '' - echo "Entering development shell for codex-cli" - # cd codex-cli - if [ -f package-lock.json ]; then - pnpm ci || echo "npm ci failed" - else - pnpm install || echo "npm install failed" - fi - npm run build || echo "npm build failed" - export PATH=$PWD/node_modules/.bin:$PATH - alias codex="node $PWD/dist/cli.js" - ''; - }; - app = { - type = "app"; - program = "${package}/bin/codex"; - }; -} - diff --git a/codex-cli/examples/README.md b/codex-cli/examples/README.md deleted file mode 100644 index 0ad83f3a73..0000000000 --- a/codex-cli/examples/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Quick start examples - -This directory bundles some self‑contained examples using the Codex CLI. If you have never used the Codex CLI before, and want to see it complete a sample task, start with running **camerascii**. You'll see your webcam feed turned into animated ASCII art in a few minutes. - -If you want to get started using the Codex CLI directly, skip this and refer to the prompting guide. - -## Structure - -Each example contains the following: -``` -example‑name/ -├── run.sh # helper script that launches a new Codex session for the task -├── task.yaml # task spec containing a prompt passed to Codex -├── template/ # (optional) starter files copied into each run -└── runs/ # work directories created by run.sh -``` - -**run.sh**: a convenience wrapper that does three things: -- Creates `runs/run_N`, where *N* is the number of a run. -- Copies the contents of `template/` into that folder (if present). -- Launches the Codex CLI with the description from `task.yaml`. - -**template/**: any existing files or markdown instructions you would like Codex to see before it starts working. - -**runs/**: the directories produced by `run.sh`. - -## Running an example - -1. **Run the helper script**: -``` -cd camerascii -./run.sh -``` -2. **Interact with the Codex CLI**: the CLI will open with the prompt: “*Take a look at the screenshot details and implement a webpage that uses a webcam to style the video feed accordingly…*” Confirm the commands Codex CLI requests to generate `index.html`. - -3. **Check its work**: when Codex is done, open ``runs/run_1/index.html`` in a browser. Your webcam feed should now be rendered as a cascade of ASCII glyphs. If the outcome isn't what you expect, try running it again, or adjust the task prompt. - - -## Other examples -Besides **camerascii**, you can experiment with: - -- **build‑codex‑demo**: recreate the original 2021 Codex YouTube demo. -- **impossible‑pong**: where Codex creates more difficult levels. -- **prompt‑analyzer**: make a data science app for clustering [prompts](https://github.com/f/awesome-chatgpt-prompts). diff --git a/codex-cli/examples/build-codex-demo/run.sh b/codex-cli/examples/build-codex-demo/run.sh deleted file mode 100755 index 5f26b191c8..0000000000 --- a/codex-cli/examples/build-codex-demo/run.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -# run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, -# then launch Codex with the task description from task.yaml. -# -# Usage: -# ./run.sh # Prompts to confirm new run -# ./run.sh --auto-confirm # Skips confirmation -# -# Assumes: -# - yq and jq are installed -# - ../task.yaml exists (with .name and .description fields) -# - ../template/ exists (optional, for bootstrapping new runs) - -# Enable auto-confirm mode if flag is passed -auto_mode=false -[[ "$1" == "--auto-confirm" ]] && auto_mode=true - -# Move into the working directory -cd runs || exit 1 - -# Grab task name for logging -task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') -echo "Checking for runs for task: $task_name" - -# Find existing run_N directories -shopt -s nullglob -run_dirs=(run_[0-9]*) -shopt -u nullglob - -if [ ${#run_dirs[@]} -eq 0 ]; then - echo "There are 0 runs." - new_run_number=1 -else - max_run_number=0 - for d in "${run_dirs[@]}"; do - [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} - done - new_run_number=$((max_run_number + 1)) - echo "There are $max_run_number runs." -fi - -# Confirm creation unless in auto mode -if [ "$auto_mode" = false ]; then - read -p "Create run_$new_run_number? (Y/N): " choice - [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 -fi - -# Create the run directory -mkdir "run_$new_run_number" - -# Check if the template directory exists and copy its contents -if [ -d "../template" ]; then - cp -r ../template/* "run_$new_run_number" - echo "Initialized run_$new_run_number from template/" -else - echo "Template directory does not exist. Skipping initialization from template." -fi - -cd "run_$new_run_number" - -# Launch Codex -echo "Launching..." -description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') -codex "$description" diff --git a/codex-cli/examples/build-codex-demo/runs/.gitkeep b/codex-cli/examples/build-codex-demo/runs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/codex-cli/examples/build-codex-demo/task.yaml b/codex-cli/examples/build-codex-demo/task.yaml deleted file mode 100644 index d18cc91f94..0000000000 --- a/codex-cli/examples/build-codex-demo/task.yaml +++ /dev/null @@ -1,88 +0,0 @@ -name: "build-codex-demo" -description: | - I want you to reimplement the original OpenAI Codex demo. - - Functionality: - - User types a prompt and hits enter to send - - The prompt is added to the conversation history - - The backend calls the OpenAI API with stream: true - - Tokens are streamed back and appended to the code viewer - - Syntax highlighting updates in real time - - When a full HTML file is received, it is rendered in a sandboxed iframe - - The iframe replaces the previous preview with the new HTML after the stream is complete (i.e. keep the old preview until a new stream is complete) - - Append each assistant and user message to preserve context across turns - - Errors are displayed to user gracefully - - Ensure there is a fixed layout is responsive and faithful to the screenshot design - - Be sure to parse the output from OpenAI call to strip the ```html tags code is returned within - - Use the system prompt shared in the API call below to ensure the AI only returns HTML - - Support a simple local backend that can: - - Read local env for OPENAI_API_KEY - - Expose an endpoint that streams completions from OpenAI - - Backend should be a simple node.js app - - App should be easy to run locally for development and testing - - Minimal setup preferred — keep dependencies light unless justified - - Description of layout and design: - - Two stacked panels, vertically aligned: - - Top Panel: Main interactive area with two main parts - - Left Side: Visual output canvas. Mostly blank space with a small image preview in the upper-left - - Right Side: Code display area - - Light background with code shown in a monospace font - - Comments in green; code aligns vertically like an IDE/snippet view - - Bottom Panel: Prompt/command bar - - A single-line text box with a placeholder prompt - - A green arrow (submit button) on the right side - - Scrolling should only be supported in the code editor and output canvas - - Visual style - - Minimalist UI, light and clean - - Neutral white/gray background - - Subtle shadow or border around both panels, giving them card-like elevation - - Code section is color-coded, likely for syntax highlighting - - Interactive feel with the text input styled like a chat/message interface - - Here's the latest OpenAI API and prompt to use: - ``` - import OpenAI from "openai"; - - const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, - }); - - const response = await openai.responses.create({ - model: "gpt-4.1", - input: [ - { - "role": "system", - "content": [ - { - "type": "input_text", - "text": "You are a coding agent that specializes in frontend code. Whenever you are prompted, return only the full HTML file." - } - ] - } - ], - text: { - "format": { - "type": "text" - } - }, - reasoning: {}, - tools: [], - temperature: 1, - top_p: 1 - }); - - console.log(response.output_text); - ``` - Additional things to note: - - Strip any html and tags from the OpenAI response before rendering - - Assume the OpenAI API model response always wraps HTML in markdown-style triple backticks like ```html ``` - - The display code window should have syntax highlighting and line numbers. - - Make sure to only display the code, not the backticks or ```html that wrap the code from the model. - - Do not inject raw markdown; only parse and insert pure HTML into the iframe - - Only the code viewer and output panel should scroll - - Keep the previous preview visible until the full new HTML has streamed in - - Add a README.md with what you've implemented and how to run it. diff --git a/codex-cli/examples/camerascii/run.sh b/codex-cli/examples/camerascii/run.sh deleted file mode 100755 index a6bcfb0328..0000000000 --- a/codex-cli/examples/camerascii/run.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, -# then launch Codex with the task description from task.yaml. -# -# Usage: -# ./run.sh # Prompts to confirm new run -# ./run.sh --auto-confirm # Skips confirmation -# -# Assumes: -# - yq and jq are installed -# - ../task.yaml exists (with .name and .description fields) -# - ../template/ exists (optional, for bootstrapping new runs) - -# Enable auto-confirm mode if flag is passed -auto_mode=false -[[ "$1" == "--auto-confirm" ]] && auto_mode=true - -# Create the runs directory if it doesn't exist -mkdir -p runs - -# Move into the working directory -cd runs || exit 1 - -# Grab task name for logging -task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') -echo "Checking for runs for task: $task_name" - -# Find existing run_N directories -shopt -s nullglob -run_dirs=(run_[0-9]*) -shopt -u nullglob - -if [ ${#run_dirs[@]} -eq 0 ]; then - echo "There are 0 runs." - new_run_number=1 -else - max_run_number=0 - for d in "${run_dirs[@]}"; do - [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} - done - new_run_number=$((max_run_number + 1)) - echo "There are $max_run_number runs." -fi - -# Confirm creation unless in auto mode -if [ "$auto_mode" = false ]; then - read -p "Create run_$new_run_number? (Y/N): " choice - [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 -fi - -# Create the run directory -mkdir "run_$new_run_number" - -# Check if the template directory exists and copy its contents -if [ -d "../template" ]; then - cp -r ../template/* "run_$new_run_number" - echo "Initialized run_$new_run_number from template/" -else - echo "Template directory does not exist. Skipping initialization from template." -fi - -cd "run_$new_run_number" - -# Launch Codex -echo "Launching..." -description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') -codex "$description" diff --git a/codex-cli/examples/camerascii/runs/.gitkeep b/codex-cli/examples/camerascii/runs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/codex-cli/examples/camerascii/task.yaml b/codex-cli/examples/camerascii/task.yaml deleted file mode 100644 index 9c5efedcf1..0000000000 --- a/codex-cli/examples/camerascii/task.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: "camerascii" -description: | - Take a look at the screenshot details and implement a webpage that uses webcam - to style the video feed accordingly (i.e. as ASCII art). Add some of the relevant features - from the screenshot to the webpage in index.html. diff --git a/codex-cli/examples/camerascii/template/screenshot_details.md b/codex-cli/examples/camerascii/template/screenshot_details.md deleted file mode 100644 index 08e41f4dec..0000000000 --- a/codex-cli/examples/camerascii/template/screenshot_details.md +++ /dev/null @@ -1,34 +0,0 @@ -### Screenshot Description - -The image is a full–page screenshot of a single post on the social‑media site X (formerly Twitter). - -1. **Header row** - * At the very top‑left is a small circular avatar. The photo shows the side profile of a person whose face is softly lit in bluish‑purple tones; only the head and part of the neck are visible. - * In the far upper‑right corner sit two standard X / Twitter interface icons: a circle containing a diagonal line (the “Mute / Block” indicator) and a three‑dot overflow menu. - -2. **Tweet body text** - * Below the header, in regular type, the author writes: - - “Okay, OpenAI’s o3 is insane. Spent an hour messing with it and built an image‑to‑ASCII art converter, the exact tool I’ve always wanted. And it works so well” - -3. **Embedded media** - * The majority of the screenshot is occupied by an embedded 12‑second video of the converter UI. The video window has rounded corners and a dark theme. - * **Left panel (tool controls)** – a slim vertical sidebar with the following labeled sections and blue–accented UI controls: - * Theme selector (“Dark” is chosen). - * A small checkbox labeled “Ignore White”. - * **Upload Image** button area that shows the chosen file name. - * **Image Processing** sliders: - * “ASCII Width” (value ≈ 143) - * “Brightness” (‑65) - * “Contrast” (58) - * “Blur (px)” (0.5) - * A square checkbox for “Invert Colors”. - * **Dithering** subsection with a checkbox (“Enable Dithering”) and a dropdown for the algorithm (value: “Noise”). - * **Character Set** dropdown (value: “Detailed (Default)”). - * **Display** slider labeled “Zoom (%)” (value ≈ 170) and a “Reset” button. - - * **Main preview area (right side)** – a dark gray canvas that renders the selected image as white ASCII characters. The preview clearly depicts a stylized **palm tree**: a skinny trunk rises from the bottom centre, and a crown of splayed fronds fills the upper right quadrant. - * A small black badge showing **“0:12”** overlays the bottom‑left corner of the media frame, indicating the video’s duration. - * In the top‑right area of the media window are two pill‑shaped buttons: a heart‑shaped “Save” button and a cog‑shaped “Settings” button. - -Overall, the screenshot shows the user excitedly announcing the success of their custom “Image to ASCII” converter created with OpenAI’s “o3”, accompanied by a short video demonstration of the tool converting a palm‑tree photo into ASCII art. diff --git a/codex-cli/examples/impossible-pong/run.sh b/codex-cli/examples/impossible-pong/run.sh deleted file mode 100755 index a6bcfb0328..0000000000 --- a/codex-cli/examples/impossible-pong/run.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, -# then launch Codex with the task description from task.yaml. -# -# Usage: -# ./run.sh # Prompts to confirm new run -# ./run.sh --auto-confirm # Skips confirmation -# -# Assumes: -# - yq and jq are installed -# - ../task.yaml exists (with .name and .description fields) -# - ../template/ exists (optional, for bootstrapping new runs) - -# Enable auto-confirm mode if flag is passed -auto_mode=false -[[ "$1" == "--auto-confirm" ]] && auto_mode=true - -# Create the runs directory if it doesn't exist -mkdir -p runs - -# Move into the working directory -cd runs || exit 1 - -# Grab task name for logging -task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') -echo "Checking for runs for task: $task_name" - -# Find existing run_N directories -shopt -s nullglob -run_dirs=(run_[0-9]*) -shopt -u nullglob - -if [ ${#run_dirs[@]} -eq 0 ]; then - echo "There are 0 runs." - new_run_number=1 -else - max_run_number=0 - for d in "${run_dirs[@]}"; do - [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} - done - new_run_number=$((max_run_number + 1)) - echo "There are $max_run_number runs." -fi - -# Confirm creation unless in auto mode -if [ "$auto_mode" = false ]; then - read -p "Create run_$new_run_number? (Y/N): " choice - [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 -fi - -# Create the run directory -mkdir "run_$new_run_number" - -# Check if the template directory exists and copy its contents -if [ -d "../template" ]; then - cp -r ../template/* "run_$new_run_number" - echo "Initialized run_$new_run_number from template/" -else - echo "Template directory does not exist. Skipping initialization from template." -fi - -cd "run_$new_run_number" - -# Launch Codex -echo "Launching..." -description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') -codex "$description" diff --git a/codex-cli/examples/impossible-pong/runs/.gitkeep b/codex-cli/examples/impossible-pong/runs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/codex-cli/examples/impossible-pong/task.yaml b/codex-cli/examples/impossible-pong/task.yaml deleted file mode 100644 index 8d8acbbf32..0000000000 --- a/codex-cli/examples/impossible-pong/task.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: "impossible-pong" -description: | - Update index.html with the following features: - - Add an overlaid styled popup to start the game on first load - - Between each point, show a 3 second countdown (this should be skipped if a player wins) - - After each game the AI wins, display text at the bottom of the screen with lighthearted insults for the player - - Add a leaderboard to the right of the court that shows how many games each player has won. - - When a player wins, a styled popup appears with the winner's name and the option to play again. The leaderboard should update. - - Add an "even more insane" difficulty mode that adds spin to the ball that makes it harder to predict. - - Add an "even more(!!) insane" difficulty mode where the ball does a spin mid court and then picks a random (reasonable) direction to go in (this should only advantage the AI player) - - Let the user choose which difficulty mode they want to play in on the popup that appears when the game starts. diff --git a/codex-cli/examples/impossible-pong/template/index.html b/codex-cli/examples/impossible-pong/template/index.html deleted file mode 100644 index 90a9e9e5cd..0000000000 --- a/codex-cli/examples/impossible-pong/template/index.html +++ /dev/null @@ -1,233 +0,0 @@ - - - - - Pong - - - - -
- - - - -
Player: 0 | AI: 0
-
- - - - - - diff --git a/codex-cli/examples/prompt-analyzer/run.sh b/codex-cli/examples/prompt-analyzer/run.sh deleted file mode 100755 index a6bcfb0328..0000000000 --- a/codex-cli/examples/prompt-analyzer/run.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, -# then launch Codex with the task description from task.yaml. -# -# Usage: -# ./run.sh # Prompts to confirm new run -# ./run.sh --auto-confirm # Skips confirmation -# -# Assumes: -# - yq and jq are installed -# - ../task.yaml exists (with .name and .description fields) -# - ../template/ exists (optional, for bootstrapping new runs) - -# Enable auto-confirm mode if flag is passed -auto_mode=false -[[ "$1" == "--auto-confirm" ]] && auto_mode=true - -# Create the runs directory if it doesn't exist -mkdir -p runs - -# Move into the working directory -cd runs || exit 1 - -# Grab task name for logging -task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') -echo "Checking for runs for task: $task_name" - -# Find existing run_N directories -shopt -s nullglob -run_dirs=(run_[0-9]*) -shopt -u nullglob - -if [ ${#run_dirs[@]} -eq 0 ]; then - echo "There are 0 runs." - new_run_number=1 -else - max_run_number=0 - for d in "${run_dirs[@]}"; do - [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} - done - new_run_number=$((max_run_number + 1)) - echo "There are $max_run_number runs." -fi - -# Confirm creation unless in auto mode -if [ "$auto_mode" = false ]; then - read -p "Create run_$new_run_number? (Y/N): " choice - [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 -fi - -# Create the run directory -mkdir "run_$new_run_number" - -# Check if the template directory exists and copy its contents -if [ -d "../template" ]; then - cp -r ../template/* "run_$new_run_number" - echo "Initialized run_$new_run_number from template/" -else - echo "Template directory does not exist. Skipping initialization from template." -fi - -cd "run_$new_run_number" - -# Launch Codex -echo "Launching..." -description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') -codex "$description" diff --git a/codex-cli/examples/prompt-analyzer/runs/.gitkeep b/codex-cli/examples/prompt-analyzer/runs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/codex-cli/examples/prompt-analyzer/task.yaml b/codex-cli/examples/prompt-analyzer/task.yaml deleted file mode 100644 index d0da4eab95..0000000000 --- a/codex-cli/examples/prompt-analyzer/task.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: "prompt-analyzer" -description: | - I have some existing work here (embedding prompts, clustering them, generating - summaries with GPT). I want to make it more interactive and reusable. - - Objective: create an interactive cluster explorer - - Build a lightweight streamlit app UI - - Allow users to upload a CSV of prompts - - Display clustered prompts with auto-generated cluster names and summaries - - Click "cluster" and see progress stream in a small window (primarily for aesthetic reasons) - - Let users browse examples by cluster, view outliers, and inspect individual prompts - - See generated analysis rendered in the app, along with the plots displayed nicely - - Support selecting clustering algorithms (e.g. DBSCAN, KMeans, etc) and "recluster" - - Include token count + histogram of prompt lengths - - Add interactive filters in UI (e.g. filter by token length, keyword, or cluster) - - When you're done, update the README.md with a changelog and instructions for how to run the app. diff --git a/codex-cli/examples/prompt-analyzer/template/Clustering.ipynb b/codex-cli/examples/prompt-analyzer/template/Clustering.ipynb deleted file mode 100644 index 4b97aa50b7..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/Clustering.ipynb +++ /dev/null @@ -1,231 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## K-means Clustering in Python using OpenAI\n", - "\n", - "We use a simple k-means algorithm to demonstrate how clustering can be done. Clustering can help discover valuable, hidden groupings within the data. The dataset is created in the [Get_embeddings_from_dataset Notebook](Get_embeddings_from_dataset.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(1000, 1536)" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# imports\n", - "import numpy as np\n", - "import pandas as pd\n", - "from ast import literal_eval\n", - "\n", - "# load data\n", - "datafile_path = \"./data/fine_food_reviews_with_embeddings_1k.csv\"\n", - "\n", - "df = pd.read_csv(datafile_path)\n", - "df[\"embedding\"] = df.embedding.apply(literal_eval).apply(np.array) # convert string to numpy array\n", - "matrix = np.vstack(df.embedding.values)\n", - "matrix.shape\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 1. Find the clusters using K-means" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We show the simplest use of K-means. You can pick the number of clusters that fits your use case best." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/opt/homebrew/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning\n", - " warnings.warn(\n" - ] - }, - { - "data": { - "text/plain": [ - "Cluster\n", - "0 4.105691\n", - "1 4.191176\n", - "2 4.215613\n", - "3 4.306590\n", - "Name: Score, dtype: float64" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from sklearn.cluster import KMeans\n", - "\n", - "n_clusters = 4\n", - "\n", - "kmeans = KMeans(n_clusters=n_clusters, init=\"k-means++\", random_state=42)\n", - "kmeans.fit(matrix)\n", - "labels = kmeans.labels_\n", - "df[\"Cluster\"] = labels\n", - "\n", - "df.groupby(\"Cluster\").Score.mean().sort_values()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from sklearn.manifold import TSNE\n", - "import matplotlib\n", - "import matplotlib.pyplot as plt\n", - "\n", - "tsne = TSNE(n_components=2, perplexity=15, random_state=42, init=\"random\", learning_rate=200)\n", - "vis_dims2 = tsne.fit_transform(matrix)\n", - "\n", - "x = [x for x, y in vis_dims2]\n", - "y = [y for x, y in vis_dims2]\n", - "\n", - "for category, color in enumerate([\"purple\", \"green\", \"red\", \"blue\"]):\n", - " xs = np.array(x)[df.Cluster == category]\n", - " ys = np.array(y)[df.Cluster == category]\n", - " plt.scatter(xs, ys, color=color, alpha=0.3)\n", - "\n", - " avg_x = xs.mean()\n", - " avg_y = ys.mean()\n", - "\n", - " plt.scatter(avg_x, avg_y, marker=\"x\", color=color, s=100)\n", - "plt.title(\"Clusters identified visualized in language 2d using t-SNE\")\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Visualization of clusters in a 2d projection. In this run, the green cluster (#1) seems quite different from the others. Let's see a few samples from each cluster." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2. Text samples in the clusters & naming the clusters\n", - "\n", - "Let's show random samples from each cluster. We'll use gpt-4 to name the clusters, based on a random sample of 5 reviews from that cluster." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from openai import OpenAI\n", - "import os\n", - "\n", - "client = OpenAI(api_key=os.environ.get(\"OPENAI_API_KEY\", \"\"))\n", - "\n", - "# Reading a review which belong to each group.\n", - "rev_per_cluster = 5\n", - "\n", - "for i in range(n_clusters):\n", - " print(f\"Cluster {i} Theme:\", end=\" \")\n", - "\n", - " reviews = \"\\n\".join(\n", - " df[df.Cluster == i]\n", - " .combined.str.replace(\"Title: \", \"\")\n", - " .str.replace(\"\\n\\nContent: \", \": \")\n", - " .sample(rev_per_cluster, random_state=42)\n", - " .values\n", - " )\n", - "\n", - " messages = [\n", - " {\"role\": \"user\", \"content\": f'What do the following customer reviews have in common?\\n\\nCustomer reviews:\\n\"\"\"\\n{reviews}\\n\"\"\"\\n\\nTheme:'}\n", - " ]\n", - "\n", - " response = client.chat.completions.create(\n", - " model=\"gpt-4\",\n", - " messages=messages,\n", - " temperature=0,\n", - " max_tokens=64,\n", - " top_p=1,\n", - " frequency_penalty=0,\n", - " presence_penalty=0)\n", - " print(response.choices[0].message.content.replace(\"\\n\", \"\"))\n", - "\n", - " sample_cluster_rows = df[df.Cluster == i].sample(rev_per_cluster, random_state=42)\n", - " for j in range(rev_per_cluster):\n", - " print(sample_cluster_rows.Score.values[j], end=\", \")\n", - " print(sample_cluster_rows.Summary.values[j], end=\": \")\n", - " print(sample_cluster_rows.Text.str[:70].values[j])\n", - "\n", - " print(\"-\" * 100)\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It's important to note that clusters will not necessarily match what you intend to use them for. A larger amount of clusters will focus on more specific patterns, whereas a small number of clusters will usually focus on largest discrepancies in the data." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "openai", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - }, - "vscode": { - "interpreter": { - "hash": "365536dcbde60510dc9073d6b991cd35db2d9bac356a11f5b64279a5e6708b97" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/codex-cli/examples/prompt-analyzer/template/README.md b/codex-cli/examples/prompt-analyzer/template/README.md deleted file mode 100644 index 0f7b18c855..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/README.md +++ /dev/null @@ -1,103 +0,0 @@ -# Prompt‑Clustering Utility - -This repository contains a small utility (`cluster_prompts.py`) that embeds a -list of prompts with the OpenAI Embedding API, discovers natural groupings with -unsupervised clustering, lets ChatGPT name & describe each cluster and finally -produces a concise Markdown report plus a couple of diagnostic plots. - -The default input file (`prompts.csv`) ships with the repo so you can try the -script immediately, but you can of course point it at your own file. - ---- - -## 1. Setup - -1. Install the Python dependencies (preferably inside a virtual env): - -```bash -pip install pandas numpy scikit-learn matplotlib openai -``` - -2. Export your OpenAI API key (**required**): - -```bash -export OPENAI_API_KEY="sk‑..." -``` - ---- - -## 2. Basic usage - -```bash -# Minimal command – runs on prompts.csv and writes analysis.md + plots/ -python cluster_prompts.py -``` - -This will - -* create embeddings with the `text-embedding-3-small` model,  -* pick a suitable number *k* via silhouette score (K‑Means), -* ask `gpt‑4o‑mini` to label & describe each cluster, -* store the results in `analysis.md`, -* and save two plots to `plots/` (`cluster_sizes.png` and `tsne.png`). - -The script prints a short success message once done. - ---- - -## 3. Command‑line options - -| flag | default | description | -|------|---------|-------------| -| `--csv` | `prompts.csv` | path to the input CSV (must contain a `prompt` column; an `act` column is used as context if present) | -| `--cache` | _(none)_ | embed­ding cache path (JSON). Speeds up repeated runs – new texts are appended automatically. | -| `--cluster-method` | `kmeans` | `kmeans` (with automatic *k*) or `dbscan` | -| `--k-max` | `10` | upper bound for *k* when `kmeans` is selected | -| `--dbscan-min-samples` | `3` | min samples parameter for DBSCAN | -| `--embedding-model` | `text-embedding-3-small` | any OpenAI embedding model | -| `--chat-model` | `gpt-4o-mini` | chat model used to generate cluster names / descriptions | -| `--output-md` | `analysis.md` | where to write the Markdown report | -| `--plots-dir` | `plots` | directory for generated PNGs | - -Example with customised options: - -```bash -python cluster_prompts.py \ - --csv my_prompts.csv \ - --cache .cache/embeddings.json \ - --cluster-method dbscan \ - --embedding-model text-embedding-3-large \ - --chat-model gpt-4o \ - --output-md my_analysis.md \ - --plots-dir my_plots -``` - ---- - -## 4. Interpreting the output - -### analysis.md - -* Overview table: cluster label, generated name, member count and description. -* Detailed section for every cluster with five representative example prompts. -* Separate lists for - * **Noise / outliers** (label `‑1` when DBSCAN is used) and - * **Potentially ambiguous prompts** (only with K‑Means) – these are items that - lie almost equally close to two centroids and might belong to multiple - groups. - -### plots/cluster_sizes.png - -Quick bar‑chart visualisation of how many prompts ended up in each cluster. - ---- - -## 5. Troubleshooting - -* **Rate‑limits / quota errors** – lower the number of prompts per run or switch - to a larger quota account. -* **Authentication errors** – make sure `OPENAI_API_KEY` is exported in the - shell where you run the script. -* **Inadequate clusters** – try the other clustering method, adjust `--k-max` - or tune DBSCAN parameters (`eps` range is inferred, `min_samples` exposed via - CLI). diff --git a/codex-cli/examples/prompt-analyzer/template/analysis.md b/codex-cli/examples/prompt-analyzer/template/analysis.md deleted file mode 100644 index 10b0882074..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/analysis.md +++ /dev/null @@ -1,23 +0,0 @@ -# Prompt Clustering Report - -Generated by `cluster_prompts.py` – 2025-04-16 - - -## Overview - -* Total prompts: **213** -* Clustering method: **kmeans** -* k (K‑Means): **2** -* Silhouette score: **0.042** -* Final clusters (excluding noise): **2** - - -| label | name | #prompts | description | -|-------|------|---------:|-------------| -| 0 | Creative Guidance Roles | 121 | This cluster encompasses a variety of roles where individuals provide expert advice, suggestions, and creative ideas across different fields. Each role, be it interior decorator, comedian, IT architect, or artist advisor, focuses on enhancing the expertise and creativity of others by tailoring advice to specific requests and contexts. | -| 1 | Role Customization Requests | 92 | This cluster contains various requests for role-specific assistance across different domains, including web development, language processing, IT troubleshooting, and creative endeavors. Each snippet illustrates a unique role that a user wishes to engage with, focusing on specific tasks without requiring explanations. | - ---- -## Plots - -The directory `plots/` contains a bar chart of the cluster sizes and a t‑SNE scatter plot coloured by cluster. diff --git a/codex-cli/examples/prompt-analyzer/template/analysis_dbscan.md b/codex-cli/examples/prompt-analyzer/template/analysis_dbscan.md deleted file mode 100644 index ff71591d69..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/analysis_dbscan.md +++ /dev/null @@ -1,22 +0,0 @@ -# Prompt Clustering Report - -Generated by `cluster_prompts.py` – 2025-04-16 - - -## Overview - -* Total prompts: **213** -* Clustering method: **dbscan** -* Final clusters (excluding noise): **1** - - -| label | name | #prompts | description | -|-------|------|---------:|-------------| -| -1 | Noise / Outlier | 10 | Prompts that do not cleanly belong to any cluster. | -| 0 | Role Simulation Tasks | 203 | This cluster consists of varied role-playing scenarios where users request an AI to assume specific professional roles, such as composer, dream interpreter, doctor, or IT architect. Each snippet showcases tasks that involve creating content, providing advice, or performing analytical functions based on user-defined themes or prompts. | - ---- - -## Plots - -The directory `plots/` contains a bar chart of the cluster sizes and a t‑SNE scatter plot coloured by cluster. diff --git a/codex-cli/examples/prompt-analyzer/template/cluster_prompts.py b/codex-cli/examples/prompt-analyzer/template/cluster_prompts.py deleted file mode 100644 index 1294194848..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/cluster_prompts.py +++ /dev/null @@ -1,547 +0,0 @@ -#!/usr/bin/env python3 -"""End‑to‑end pipeline for analysing a collection of text prompts. - -The script performs the following steps: - -1. Read a CSV file that must contain a column named ``prompt``. If an - ``act`` column is present it is used purely for reporting purposes. -2. Create embeddings via the OpenAI API (``text-embedding-3-small`` by - default). The user can optionally provide a JSON cache path so the - expensive embedding step is only executed for new / unseen texts. -3. Cluster the resulting vectors either with K‑Means (automatically picking - *k* through the silhouette score) or with DBSCAN. Outliers are flagged - as cluster ``-1`` when DBSCAN is selected. -4. Ask a Chat Completion model (``gpt-4o-mini`` by default) to come up with a - short name and description for every cluster. -5. Write a human‑readable Markdown report (default: ``analysis.md``). -6. Generate a couple of diagnostic plots (cluster sizes and a t‑SNE scatter - plot) and store them in ``plots/``. - -The script is intentionally opinionated yet configurable via a handful of CLI -options – run ``python cluster_prompts.py --help`` for details. -""" - -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path -from typing import Any, Sequence - -import numpy as np -import pandas as pd - -# External, heavy‑weight libraries are imported lazily so that users running the -# ``--help`` command do not pay the startup cost. - - -def parse_cli() -> argparse.Namespace: # noqa: D401 - """Parse command‑line arguments.""" - - parser = argparse.ArgumentParser( - prog="cluster_prompts.py", - description="Embed, cluster and analyse text prompts via the OpenAI API.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - - parser.add_argument("--csv", type=Path, default=Path("prompts.csv"), help="Input CSV file.") - parser.add_argument( - "--cache", - type=Path, - default=None, - help="Optional JSON cache for embeddings (will be created if it does not exist).", - ) - parser.add_argument( - "--embedding-model", - default="text-embedding-3-small", - help="OpenAI embedding model to use.", - ) - parser.add_argument( - "--chat-model", - default="gpt-4o-mini", - help="OpenAI chat model for cluster descriptions.", - ) - - # Clustering parameters - parser.add_argument( - "--cluster-method", - choices=["kmeans", "dbscan"], - default="kmeans", - help="Clustering algorithm to use.", - ) - parser.add_argument( - "--k-max", - type=int, - default=10, - help="Upper bound for k when the kmeans method is selected.", - ) - parser.add_argument( - "--dbscan-min-samples", - type=int, - default=3, - help="min_samples parameter for DBSCAN (only relevant when dbscan is selected).", - ) - - # Output paths - parser.add_argument( - "--output-md", type=Path, default=Path("analysis.md"), help="Markdown report path." - ) - parser.add_argument( - "--plots-dir", type=Path, default=Path("plots"), help="Directory that will hold PNG plots." - ) - - return parser.parse_args() - - -# --------------------------------------------------------------------------- -# Embedding helpers -# --------------------------------------------------------------------------- - - -def _lazy_import_openai(): # noqa: D401 - """Import *openai* only when needed to keep startup lightweight.""" - - try: - import openai # type: ignore - - return openai - except ImportError as exc: # pragma: no cover – we do not test missing deps. - raise SystemExit( - "The 'openai' package is required but not installed.\n" - "Run 'pip install openai' and try again." - ) from exc - - -def embed_texts(texts: Sequence[str], model: str, batch_size: int = 100) -> list[list[float]]: - """Embed *texts* with OpenAI and return a list of vectors. - - Uses batching for efficiency but remains on the safe side regarding current - OpenAI rate limits (can be adjusted by changing *batch_size*). - """ - - openai = _lazy_import_openai() - client = openai.OpenAI() - - embeddings: list[list[float]] = [] - - for batch_start in range(0, len(texts), batch_size): - batch = texts[batch_start : batch_start + batch_size] - - response = client.embeddings.create(input=batch, model=model) - # The API returns the vectors in the same order as the input list. - embeddings.extend(data.embedding for data in response.data) - - return embeddings - - -def load_or_create_embeddings( - prompts: pd.Series, *, cache_path: Path | None, model: str -) -> pd.DataFrame: - """Return a *DataFrame* with one row per prompt and the embedding columns. - - * If *cache_path* is provided and exists, known embeddings are loaded from - the JSON cache so they don't have to be re‑generated. - * Missing embeddings are requested from the OpenAI API and subsequently - appended to the cache. - * The returned DataFrame has the same index as *prompts*. - """ - - cache: dict[str, list[float]] = {} - if cache_path and cache_path.exists(): - try: - cache = json.loads(cache_path.read_text()) - except json.JSONDecodeError: # pragma: no cover – unlikely. - print("⚠️ Cache file exists but is not valid JSON – ignoring.", file=sys.stderr) - - missing_mask = ~prompts.isin(cache) - - if missing_mask.any(): - texts_to_embed = prompts[missing_mask].tolist() - print(f"Embedding {len(texts_to_embed)} new prompt(s)…", flush=True) - new_embeddings = embed_texts(texts_to_embed, model=model) - - # Update cache (regardless of whether we persist it to disk later on). - cache.update(dict(zip(texts_to_embed, new_embeddings))) - - if cache_path: - cache_path.parent.mkdir(parents=True, exist_ok=True) - cache_path.write_text(json.dumps(cache)) - - # Build a consistent embeddings matrix - vectors = prompts.map(cache.__getitem__).tolist() # type: ignore[arg-type] - mat = np.array(vectors, dtype=np.float32) - return pd.DataFrame(mat, index=prompts.index) - - -# --------------------------------------------------------------------------- -# Clustering helpers -# --------------------------------------------------------------------------- - - -def _lazy_import_sklearn_cluster(): - """Lazy import helper for scikit‑learn *cluster* sub‑module.""" - - # Importing scikit‑learn is slow; defer until needed. - from sklearn.cluster import DBSCAN, KMeans # type: ignore - from sklearn.metrics import silhouette_score # type: ignore - from sklearn.preprocessing import StandardScaler # type: ignore - - return KMeans, DBSCAN, silhouette_score, StandardScaler - - -def cluster_kmeans(matrix: np.ndarray, k_max: int) -> np.ndarray: - """Auto‑select *k* (in ``[2, k_max]``) via Silhouette score and cluster.""" - - KMeans, _, silhouette_score, _ = _lazy_import_sklearn_cluster() - - best_k = None - best_score = -1.0 - best_labels: np.ndarray | None = None - - for k in range(2, k_max + 1): - model = KMeans(n_clusters=k, random_state=42, n_init="auto") - labels = model.fit_predict(matrix) - try: - score = silhouette_score(matrix, labels) - except ValueError: - # Occurs when a cluster ended up with 1 sample – skip. - continue - - if score > best_score: - best_k = k - best_score = score - best_labels = labels - - if best_labels is None: # pragma: no cover – highly unlikely. - raise RuntimeError("Unable to find a suitable number of clusters.") - - print(f"K‑Means selected k={best_k} (silhouette={best_score:.3f}).", flush=True) - return best_labels - - -def cluster_dbscan(matrix: np.ndarray, min_samples: int) -> np.ndarray: - """Cluster with DBSCAN; *eps* is estimated via the k‑distance method.""" - - _, DBSCAN, _, StandardScaler = _lazy_import_sklearn_cluster() - - # Scale features – DBSCAN is sensitive to feature scale. - scaler = StandardScaler() - matrix_scaled = scaler.fit_transform(matrix) - - # Heuristic: use the median of the distances to the ``min_samples``‑th - # nearest neighbour as eps. This is a commonly used rule of thumb. - from sklearn.neighbors import NearestNeighbors # type: ignore # lazy import - - neigh = NearestNeighbors(n_neighbors=min_samples) - neigh.fit(matrix_scaled) - distances, _ = neigh.kneighbors(matrix_scaled) - kth_distances = distances[:, -1] - eps = float(np.percentile(kth_distances, 90)) # choose a high‑ish value. - - print(f"DBSCAN min_samples={min_samples}, eps={eps:.3f}", flush=True) - model = DBSCAN(eps=eps, min_samples=min_samples) - return model.fit_predict(matrix_scaled) - - -# --------------------------------------------------------------------------- -# Cluster labelling helpers (LLM) -# --------------------------------------------------------------------------- - - -def label_clusters( - df: pd.DataFrame, labels: np.ndarray, chat_model: str, max_examples: int = 12 -) -> dict[int, dict[str, str]]: - """Generate a name & description for each cluster label via ChatGPT. - - Returns a mapping ``label -> {"name": str, "description": str}``. - """ - - openai = _lazy_import_openai() - client = openai.OpenAI() - - out: dict[int, dict[str, str]] = {} - - for lbl in sorted(set(labels)): - if lbl == -1: - # Noise (DBSCAN) – skip LLM call. - out[lbl] = { - "name": "Noise / Outlier", - "description": "Prompts that do not cleanly belong to any cluster.", - } - continue - - # Pick a handful of example prompts to send to the model. - examples_series = df.loc[labels == lbl, "prompt"].sample( - min(max_examples, (labels == lbl).sum()), random_state=42 - ) - examples = examples_series.tolist() - - user_content = ( - "The following text snippets are all part of the same semantic cluster.\n" - "Please propose \n" - "1. A very short *title* for the cluster (≤ 4 words).\n" - "2. A concise 2–3 sentence *description* that explains the common theme.\n\n" - "Answer **strictly** as valid JSON with the keys 'name' and 'description'.\n\n" - "Snippets:\n" - ) - user_content += "\n".join(f"- {t}" for t in examples) - - messages = [ - { - "role": "system", - "content": "You are an expert analyst, competent in summarising text clusters succinctly.", - }, - {"role": "user", "content": user_content}, - ] - - try: - resp = client.chat.completions.create(model=chat_model, messages=messages) - reply = resp.choices[0].message.content.strip() - - # Extract the JSON object even if the assistant wrapped it in markdown - # code fences or added other text. - - # Remove common markdown fences. - reply_clean = reply.strip() - # Take the substring between the first "{" and the last "}". - m_start = reply_clean.find("{") - m_end = reply_clean.rfind("}") - if m_start == -1 or m_end == -1: - raise ValueError("No JSON object found in model reply.") - - json_str = reply_clean[m_start : m_end + 1] - data = json.loads(json_str) # type: ignore[arg-type] - - out[lbl] = { - "name": str(data.get("name", "Unnamed"))[:60], - "description": str(data.get("description", "")).strip(), - } - except Exception as exc: # pragma: no cover – network / runtime errors. - print(f"⚠️ Failed to label cluster {lbl}: {exc}", file=sys.stderr) - out[lbl] = {"name": f"Cluster {lbl}", "description": ""} - - return out - - -# --------------------------------------------------------------------------- -# Reporting helpers -# --------------------------------------------------------------------------- - - -def generate_markdown_report( - df: pd.DataFrame, - labels: np.ndarray, - meta: dict[int, dict[str, str]], - outputs: dict[str, Any], - path_md: Path, -): - """Write a self‑contained Markdown analysis to *path_md*.""" - - path_md.parent.mkdir(parents=True, exist_ok=True) - - cluster_ids = sorted(set(labels)) - counts = {lbl: int((labels == lbl).sum()) for lbl in cluster_ids} - - lines: list[str] = [] - - lines.append("# Prompt Clustering Report\n") - lines.append(f"Generated by `cluster_prompts.py` – {pd.Timestamp.now()}\n") - - # High‑level stats - total = len(labels) - num_clusters = len(cluster_ids) - (1 if -1 in cluster_ids else 0) - lines.append("\n## Overview\n") - lines.append(f"* Total prompts: **{total}**") - lines.append(f"* Clustering method: **{outputs['method']}**") - if outputs.get("k"): - lines.append(f"* k (K‑Means): **{outputs['k']}**") - lines.append(f"* Silhouette score: **{outputs['silhouette']:.3f}**") - lines.append(f"* Final clusters (excluding noise): **{num_clusters}**\n") - - # Summary table - lines.append("\n| label | name | #prompts | description |") - lines.append("|-------|------|---------:|-------------|") - for lbl in cluster_ids: - meta_lbl = meta[lbl] - lines.append(f"| {lbl} | {meta_lbl['name']} | {counts[lbl]} | {meta_lbl['description']} |") - - # Detailed section per cluster - for lbl in cluster_ids: - lines.append("\n---\n") - meta_lbl = meta[lbl] - lines.append(f"### Cluster {lbl}: {meta_lbl['name']} ({counts[lbl]} prompts)\n") - lines.append(f"{meta_lbl['description']}\n") - - # Show a handful of illustrative prompts. - sample_n = min(5, counts[lbl]) - examples = df.loc[labels == lbl, "prompt"].sample(sample_n, random_state=42).tolist() - lines.append("\nExamples:\n") - lines.extend([f"* {t}" for t in examples]) - - # Outliers / ambiguous prompts, if any. - if -1 in cluster_ids: - lines.append("\n---\n") - lines.append(f"### Noise / outliers ({counts[-1]} prompts)\n") - examples = ( - df.loc[labels == -1, "prompt"].sample(min(10, counts[-1]), random_state=42).tolist() - ) - lines.extend([f"* {t}" for t in examples]) - - # Optional ambiguous set (for kmeans) - ambiguous = outputs.get("ambiguous", []) - if ambiguous: - lines.append("\n---\n") - lines.append(f"### Potentially ambiguous prompts ({len(ambiguous)})\n") - lines.extend([f"* {t}" for t in ambiguous]) - - # Plot references - lines.append("\n---\n") - lines.append("## Plots\n") - lines.append( - "The directory `plots/` contains a bar chart of the cluster sizes and a t‑SNE scatter plot coloured by cluster.\n" - ) - - path_md.write_text("\n".join(lines)) - - -# --------------------------------------------------------------------------- -# Plotting helpers -# --------------------------------------------------------------------------- - - -def create_plots( - matrix: np.ndarray, - labels: np.ndarray, - for_devs: pd.Series | None, - plots_dir: Path, -): - """Generate cluster size and t‑SNE plots.""" - - import matplotlib.pyplot as plt # type: ignore – heavy, lazy import. - from sklearn.manifold import TSNE # type: ignore – heavy, lazy import. - - plots_dir.mkdir(parents=True, exist_ok=True) - - # Bar chart with cluster sizes - unique, counts = np.unique(labels, return_counts=True) - order = np.argsort(-counts) # descending - unique, counts = unique[order], counts[order] - - plt.figure(figsize=(8, 4)) - plt.bar([str(u) for u in unique], counts, color="steelblue") - plt.xlabel("Cluster label") - plt.ylabel("# prompts") - plt.title("Cluster sizes") - plt.tight_layout() - bar_path = plots_dir / "cluster_sizes.png" - plt.savefig(bar_path, dpi=150) - plt.close() - - # t‑SNE scatter - tsne = TSNE( - n_components=2, perplexity=min(30, len(matrix) // 3), random_state=42, init="random" - ) - xy = tsne.fit_transform(matrix) - - plt.figure(figsize=(7, 6)) - scatter = plt.scatter(xy[:, 0], xy[:, 1], c=labels, cmap="tab20", s=20, alpha=0.8) - plt.title("t‑SNE projection") - plt.xticks([]) - plt.yticks([]) - - if for_devs is not None: - # Overlay dev prompts as black edge markers - dev_mask = for_devs.astype(bool).values - plt.scatter( - xy[dev_mask, 0], - xy[dev_mask, 1], - facecolors="none", - edgecolors="black", - linewidths=0.6, - s=40, - label="for_devs = TRUE", - ) - plt.legend(loc="best") - - tsne_path = plots_dir / "tsne.png" - plt.tight_layout() - plt.savefig(tsne_path, dpi=150) - plt.close() - - -# --------------------------------------------------------------------------- -# Main entry point -# --------------------------------------------------------------------------- - - -def main() -> None: # noqa: D401 - args = parse_cli() - - # Read CSV – require a 'prompt' column. - df = pd.read_csv(args.csv) - if "prompt" not in df.columns: - raise SystemExit("Input CSV must contain a 'prompt' column.") - - # Keep relevant columns only for clarity. - df = df[[c for c in df.columns if c in {"act", "prompt", "for_devs"}]] - - # --------------------------------------------------------------------- - # 1. Embeddings (may be cached) - # --------------------------------------------------------------------- - embeddings_df = load_or_create_embeddings( - df["prompt"], cache_path=args.cache, model=args.embedding_model - ) - - # --------------------------------------------------------------------- - # 2. Clustering - # --------------------------------------------------------------------- - mat = embeddings_df.values.astype(np.float32) - - if args.cluster_method == "kmeans": - labels = cluster_kmeans(mat, k_max=args.k_max) - else: - labels = cluster_dbscan(mat, min_samples=args.dbscan_min_samples) - - # Identify potentially ambiguous prompts (only meaningful for kmeans). - outputs: dict[str, Any] = {"method": args.cluster_method} - if args.cluster_method == "kmeans": - from sklearn.cluster import KMeans # type: ignore – lazy - - best_k = len(set(labels)) - # Re‑fit KMeans with the chosen k to get distances. - kmeans = KMeans(n_clusters=best_k, random_state=42, n_init="auto").fit(mat) - outputs["k"] = best_k - # Silhouette score (again) – not super efficient but okay. - from sklearn.metrics import silhouette_score # type: ignore - - outputs["silhouette"] = silhouette_score(mat, labels) - - distances = kmeans.transform(mat) - # Ambiguous if the ratio between 1st and 2nd closest centroid < 1.1 - sorted_dist = np.sort(distances, axis=1) - ratio = sorted_dist[:, 0] / (sorted_dist[:, 1] + 1e-9) - ambiguous_mask = ratio > 0.9 # tunes threshold – close centroids. - outputs["ambiguous"] = df.loc[ambiguous_mask, "prompt"].tolist() - - # --------------------------------------------------------------------- - # 3. LLM naming / description - # --------------------------------------------------------------------- - meta = label_clusters(df, labels, chat_model=args.chat_model) - - # --------------------------------------------------------------------- - # 4. Plots - # --------------------------------------------------------------------- - create_plots(mat, labels, df.get("for_devs"), args.plots_dir) - - # --------------------------------------------------------------------- - # 5. Markdown report - # --------------------------------------------------------------------- - generate_markdown_report(df, labels, meta, outputs, path_md=args.output_md) - - print(f"✅ Done. Report written to {args.output_md} – plots in {args.plots_dir}/", flush=True) - - -if __name__ == "__main__": - # Guard the main block to allow safe import elsewhere. - main() diff --git a/codex-cli/examples/prompt-analyzer/template/plots/cluster_sizes.png b/codex-cli/examples/prompt-analyzer/template/plots/cluster_sizes.png deleted file mode 100644 index 5d5d012c4355c56d539054950727a00d0f6ab343..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19000 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 z 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ℨp)+d)^B`7s#v8axi)@AVwi{$kg1LS0Lt+` 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 diff --git a/codex-cli/examples/prompt-analyzer/template/plots_dbscan/cluster_sizes.png b/codex-cli/examples/prompt-analyzer/template/plots_dbscan/cluster_sizes.png deleted file mode 100644 index f8cf4006dae8be9021d45f0bbff274af533255fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20441 zcmeIaXH-*Zzc(Colo1tl6p%jZpwdM^qy%ip00K&{0c8;B(g~qC78C?j3?N-PBoI&_ zlt54^N(<7ZB+?-PVh9i*Bze~r@44SO=Q;1Q-nGtipL0GuA6!V;+1K9t`j=n%@259R z^bhih@{calo zfe8Ns{_(hF_nAQ;rg{cfFIj|UEKlsq$eM1{WmA9q?Zu-*Mwfqo@|*NePpkG_&Tsi8 z_BXPP?8cRwu`f%DW0UhXaxLv+`+l)ESk`(mQgT<}(rp8Sr_xV3Gb*XebZFCQ!Bl5*;>K)o&7~)Uo-jyJ_|?%h5nieAdy!DTsg@2x*+^gLoSJLLs~s(`%2uJRy1o#viuZf{7T(0P zZPKQN-Mf^UHg8M1Y_qH!{95eyImp!P88oFcbevt&n$OtW{eKBHCc=!TVgnl6W~OIzzV9o{@(UStyAZ>kFo zTU&U6T1!)P|HXT(CVpk2q5Xxvq=*1?t@i{+64LxOZ`XcEFs(H@a4}5Yh`36a=8=KRK^U(z-KG}MfRZpQXO&xC>@~*|- zj#nEhUWF^z*PvI~?|G%3H8O}=I?SbaJ35c_Wy0a}nIWse9}lXxAgy5{ypo~L@h;cn zsf>vb(&jb=6A+rzhP@C^519xW4*hnYS2nTWkoWf5B5koKd#)ryofICx-!G&W*0d%z z@S8E~`f*F+#*e|I2u8=nvzA0PU!pdiLviAywbLk09L-2JJ=?lYDU%dNDWj848sHIO zk2yQ@x?A|lsGVskc>Od3&td6>?S(jhy?K8RWB{J1z507Na~Zp}4nr&p>LqLCQ?mN`KR!LzV|Vw_yO~{r_Ixj7WtYqf z2F4G5J8haJuC-ic&Y!pUpy;b??1`VRPao3>59O~ifvyiY+*kJ!$k-Bw=dh@GAyi`A&VheE{<$p4ltO}FHwx2~wmVbR_4}wNrp)Z?tAzEXuPVMq z;}?fe5EK2HUcGj1=pB>(Qj3xBg7$D+w7Pu{W^d-z^r#>*y!q zZ*`?A!?R^^bLDRh1$|^vjowea{aLtQwe{%3g6H0uK|`OXjR(C1`;K-b>as6vjQQd> z>De5IS)yu!3PD@N$}wp0!sNpv{?66&S%@CSfkpLR;)8E_M8t1t^SRWDx05N zl$W3>%Xnk>-pMZL+#?%7Gt04fbz-Vhwtt7rewG3(a%@))Cu(J1~Q-`ywOr+)Dl-fMzcVdN5=srec6J(k9@mr(;rvXAZtm|CNs zZ|f?Q3~*%Cj7BfQdWCyO45o871T~(ZuEy(3VSG&de?kC#e2QP zZ>Nz#OfULgKrSA@%c)^ZBRZ0=dGtAb34}%vBoRwDr+r_@FJMy}$Q~=C6P7uw?=iYv z#!Kc@%+Z#DQuI;P!T=1Oy=?~F4XK#<2|SPCGJ9;tFi31izuZapPDX|G8Aj=g`(5>V z`}tIzuVq?aN*cO|uWaC1grB8_Re@~oz;BT^YX0(}{et6Tm-lamAi7i*%_{gt?ZYy~iU>biNznr@x#M)j*;ZB#0r&`He zM;*(G++Kutxvzg1__&0l_jGbHGo#pBMw=7}#OO*i2>3Byd_ z{U*x9uFm(8S&&&;a&W7l^-P6F$153HjPA~H#&Q_7k#T#W_e~tMyvBQME~2!;uz$Cw zecHLyw2mVDmD0elS){f9g2hceWNEjMeyNR-oP6O$**%6SPLW%eqvB$j$)~(_1~HLH zgm(e*sQIVb<(jd%X!%EhB_TqP8+M=u_81N7Kj0PU!I&SRsuM$9UQmrKFT|ZFb4bb0 zDLFOW79ZD|xxI zJ~1L`t-^ImmV6)T0~cTJuCS1{OoBCr%|1VCaFCc{dGEE=Z{pCfK-ltGmV#N%9>}X? z$bLw%Fo@^7U*Ecx4;WaQNQHlYzYCMC8RWs=C}gFk7Gi0lmt8>qvlPTEuiD^iHP4}4 zx`+a#Rhm+jp=?@Ak!(&{;L3QNzv#G9BrWcd4q=sixTctJfk9XJq)rM?DiSYTM93@{ z@tEm7u}P{rcZ=MJ%)fE3FW(?;-Clk{#4OZOxz+z>HV?!mc*n^Jk^}o)P;=NRDx$nu zL^m?rt7&Uaqa~D5hLyz_SQ?ge$gHc6IzKo#RWs&eKSMA@eF<9VJ&rdqXJsK)>Kx*e z(u#Dq7xFZ&8e&jMqP0bQ`Km$Rm$9Vsb6KECP!BTQURyMm&Nqm7D=YiLr)g_Ww0)z2 z#8Qk2DOp8_P>Fu?nITrM$*CW3Yh(F_x#lJs(#bIcbn^2n{GTnI19*L8elwiBotBVt zK>WhJ7dq71>4Z2Y6|c*I_LJN{9#nV8XGYS~Ni*)x+znl~q5T0-TeB(1`TnZzffz4L z<3?-*V*=aJxJXSGZB3NWsxeGOvc*QDIIMt=jH9pu!QXhIf05&b6zgY&uYO)Vs6KwS zyACy;rs{6iH0psus(TC+uiDt!+HN&5SL}5V8%rGsDfrBb4*FmngNW#~cJOQM@Jv(p zg%Q!V?{?|~=6hzFoe#D$!^+fQPfw%tz(upI#>Ehm zD2|YVorMU2Uu)^cY*QVf009a>2i~_eN`=l|fn0Ro!HUU1E<#_(X7+hRVaNTA#v>RL z@Oq~-VoK>fvv+G%qa@Ib>27~X%=H{Dk6Zs*vO4$lbk-+9JyYnqMfoRN-^TS-ixzWJ zs9IJOd&`4rWc&%!uu>m7^Fn{>kudDZ=cLo~9r;#ecC*S$P0SU)F6DO67|%DTuSaOo zyHw~$>s6)8b2YYb{e1laMSN&oHZ75c#j_Nw-xPTE4K&7|A3YPcu_OXA{ZeJSWTke# zY=uR{`iM(nYr1cf2#3wU2MF4fJ|6Y&R0tTfOB=1L?JIp$!aMY5SL3$x%Pe)ZL{5J; z`xUCJuk_DFhRL8q$xF{Tg_o2p%+GK$e z``-?p0BkFR?X^+4)AdAarS>F0bgg$`wAxeEb4cE<>h9rm?;6_-_}pt{@6jqzs)UCB zAK{D!Qhc(4eVYfWOsHX{-t(D=%ANZp#(=r$ETwOKu_=%fH0+2K&pH4Q^dc;el>_}c($0T9A_6EyYV^ID{*?UD%jA%}o&S%yT^4$Y6&D%@@;S9Wyw z-UClwtGKmY5r7_DJT0o`VX;&EI;?7`E<@%Z!2*hZnX>zwG;%5ltQ4Q{0T$t_s>Zw?{w!`}3 zhcS&Cc0`a5YZ*ku^!l(o>_qUmD`v>9(Q1JU zy|q)Za{lXs+Z7PvS5~ zfRHT13OdT68zU$89u&2JhnH0k#Rd?d>UfjmT&QkT4%=(9|K+x`^t(IVv8Fm(zv05d#G1KUr^WsZw)IQ*dptv9zIaEC z?z3u+rhNk+j@~V+Y!m&`6v5nDS^ScxPsFcw$XO;7l{^l}&bCiFW$aHaI|^&0wTX4k z+JYS=`sZF@=K>w;&pldHapUy!-I&wHX|EWAkE{*s-d6-shLnTI1LHsG7?|_wa(3u@ zh}M4LQ!P(BCk?nqKNig8*b^u=8^Zhva@)B8g0e(e zAsO-uO@XFnhr<*++Szd9)U(GmgUD6CXLAFKq?*cyt~&RPhgba2?CdOUjKr>aE6lw~ ztwX=cR_JVgd!X%l)|f|o^5K!G+UW$8f0JuxGF;cC?U|^q%l6tLJi8P#*HkpsgtUgi z-!SC60E9)I1KdZ>@nVLcO?KqA`+;L;e`UPU-2C=n8b|L*r>3nB*(*^=Q5=?s?#Vqj z-M7D$qRQ<=t`#(M@%6U|!FH{{g^I2$Uhpw*N``r?$mZ3ZiVZEEm`0%odk@CSVQ}X< zIy&YW$~}k6>)tS5$(Z|B58+^;laFvzVj}`ME!clZu7|Kp!Qd0Dog&l#ly|HTw0&#} z(qC`Iz!=1H2yI*0E{SMSA!r`~h-o~q-x%yw4~(OG0Wvp=!`@m~z>c3IZQabfHp^OR zpu%Q1oA2*NWn_;{gpecv0F($GIUd7heB@t;i8yz^Ryn;f5kd;xoQ%oyB{lhtd1LjG zP8kb@nv&y@89_s~w7GO&E6J$s#iHq0x$-ANW%liFVkGTmvp(``$(u^!=sjBW5R1*t z>BQ-oB-0*@sWz>}bte!!^@B!@UQ>~a1u1RISW=b~fd8~OsYNr#F5gSVYIIng*8ipaf`77sYMmk?0xPCxFGf+fV1TbD7&8@OJ;?7k0 zOw`Y^Hfhs&iev6A3!{>|Ao4Gmk2zSXGz3zeK(#Iaby|V+Y5R7aZMpqqI9SkARf299~Bo} zdv@$_2oXMi!bvx9p;wd;FA3FT)3Z5ONlg(bD5=D}Xyi`1cPW$~WVOB$ERR*TPDCz( zvGN6Fjll&ijVRygPP3d|KdGO9-q>`@ju494>U3JA?B>6aGwNv;YT3^M3zp9e6LZe= z?~v)73Gk_#O%@L?Sd7lp3W1%<-UmK87lnmZBqPOqYL8Q+*7BqVe-wGq{4o3b)$-TV zRAsV4r=HsQwFeWf%DuAzPKb;-^3kuT|mSt8;@6poFyS91>TwH;4vt6_< zXNzzX29pGBRh4^xVH>6GnMe}0&bxlG(2r7vO$!Fey5iMOHb%-3_mGxWPXXDd9QyrR zM-I6l1s3bHGbOAXyfkv`tn~i~~6ZJu;jm?Mqj-c;?Lse~dp+#}uKkTQCdSdh_ zLuD?Ei4e?28mM@_0TdEGN_%T2sgkuZHoq8?;uHzbg8e8Dhn*Gjw>_whjD$ktbj6`Y z&>N$EErd`&Gd=_H+}WhXS-$2bqC*SkVDTCOb1hEn)s&s>=-vD_xrE|WvkUOm8CT8o z;9?(Ni?)o9kqkS-Ybv1`_@pU8@K}29WDpyxmL7koYuwSWb za`>jcM72XZ$lfpR-?v<8m`>2TJzC`s zHp{|jHRbYdbdrgtjI}M`LX`vNx;EQ&*@vk(x{0dCz!}B5nU@P+-_4(k`f*kH#Jx7= zh1Z|@O}BN2gs-?PrtLA7a!5U5s;#_&=df{M-$1f=ied#-c@8U*m`T|?zr{HJ*lurE zRp;X`Q-;dyiA6c+f0Ie9%SQG>a;m;Q`jJQeXYug=L2&;UDFA;z^WYeSvNfNb2*~)o z2StxAk6{ymG%z^xOr@t7S6J^oQgKU-$Q&>?Z7pwD5k-~N*j0IbJ=esXptga=HRDI) zddv1Wx#aN4Yb}`}#JUlNcjNj{<>X_ro)DZHA!`s8MJdC!=`fd$kRqAhUJc7NmD^-g zJ7`j`#PG*k!{E|AT2!30s4iICx^^?UzsNL@N`!{3labg239UA??K`0`(ybT?Azq2V z-V+h+McI*}G2;HecfD3Q&1+jSDz~+emv}qfyXGmKpsO1^=8at##*oZ(fkbohily&v zw7H3fYh6^krM07=Q#efzpfBX4<=Y`U&(^y6tn|c!Lq=RdrecQdEr2#V1`P`&3W%Kh zqbXoFFaNVU-D#~41ayw6xb>hPeF2d)Ah2Gu%hLcbiL^c=rP*E?wJ4HnilfI;)cR_t z(tJdsBy>(|INu;?OPlCb-Mf2Oot^CFYDt*hL7{QyGYXnc#_ zJ-0s_96b`rTz2U@XlaJng?rA#L{T-5Cgw^Lb0t+H;LcLTM}CbKuz2Us zKUbW%-SJAs@2hJ{H`dPC&i#9FUP7$jberB-jkim0&Lztpso;@YaM{Vs5TXcnBG`VG z1}H2HEHxu^>Zx=XIBE2D3Guw26f`XG*)iqZRo2qicl5{=%b#Kdq_R-OX7AQr5;RUd zJ}4&C7`EOKKW1KxEZiHp`uX&i`XGvf>mUwJmnBvH@A@v?{{?@%qmjFc zEi}|Hb%_O@FOO=1(h3Sy=ybgL?XFa1(4^W#vDT+^9`MIzv)76$do(E)Oy~n%frNIc zC~d&ig-7vqoF|4(i$6Iip&6*5bDZ0R-LT3^)4q9kqCTjTNtYEWCVeDoNnmd{Xa;%{Cnp5SE#0jqR!o^$|nI+(O+bW@ed~tlqh?RRE&6IC#KqU z*?!kk&%Sx}kt^;tTb-Fri9e=%q6f+t51o7Mmg`2y+8ROIzRL}v5@|#BzIt#tT<5&U zKym2SY)YPELvVq(B)Eiwjk_q5F;WR8nVOc5tv?@BA3x;Vv~|Y|&)V=KMKD@TBUh6) z2~q4L!m!X=GBH3tzjz6$+HBa*s}-{1Fv}HD&dABdd14yR2ao%;*DmD7_!hzN`ojvNNWG9_ouVN&(()eB`M_%KC`s$^JDTv1hvtO{~*}Zf6BT? z57u~(h0bS&$dUlY)EF4`X;?0GNRfRDJ}qa4XJ{_nH%qLR6jXVb$4Hdk6x2%c=(eUG~k`H~{?} zgfbta?nRJNi?5-FNl3Lqy%s5M9o`6`S>Eg={60o2GGB>iTOSQB35Ph`7sMb6kmO*R!YKdkTYU-q04GU zWkK(Afr_KA8a^l)T~xFWrcVvrH+h8XY{`LAp1VV6TmF0@ zh97>Li~=b%oV_&<@Ax*(s_$bS4FyqIx7X%BjzlS&u`3YBnX1`L$5&OnLqWri!Y*yk zY!`w6yc!mI4_F_^`oPZ^-JS0rMt@8vN_54 zUTRV*n~r_!1~}tkk)|;_t!iy(;yHa{vH^ zu6{lp5WqN{6?#Guzq6{{2tF|S&+^M7#W(Y`CC)||w?^Iq)Y-UxseYnl^;^ElVW%hw z#8RdHcmM$kHcbnQLCQ|AQ%d-fr{C_=u$*kIa2+M6co21H*TvPbs&D%NrwifaO3}X?;hNT%4Co{{(`SjD5*#!`W=toyvK@-+?-OO3--Fd?d-M z=Ib40|Cvt<#73rBgsDfa&h3^b9gKHwgdXOA6Y9f1AOl?fbXMqXof`HwW&T%`W{gCz z4gc3U{t6S##e4@>_!heML{tgjIVCl zdd!VwE`3qy(jM+J)$$ZR>d2S0y-rDyt7@WWQsrqO#;?ywx5bO8lW5#3s+tx1>+)hz zc8`5fU+o-!h1mkq>dbYC4WM`6!frJLFHZrZNcPUmX@F;>CAn=W$I=S+V*J&`G@lUW zGPb?q*2k$X6~Z$?tKzh5!1b8uveqszm)^Nx8aGOpP7d}ePz%l&xfUJH1N3Rd)fF+F z3c#rL=Ddr$c5~{~%Hn+>`*aDzqyZ@GXY{G*`EGLK*Zq}hv!VHtPyyDm{*)sqz!2$! z6=n}M_8mDTzg8b;T+ij@3`Lz;Q^NQsZrKs%%j@RSL@{+6qn>oNqO35Ff7Xpa!4EsA`zsHYQkVY6(IQD1!okTmB_88>P` zv5Cc8$IM&x)9S2cyq=&h{|u?_lzj++RHr+>s@g?q-YtMu&H$S-+D@RNuL?kO$QiFA zA>gZx!#@a|vV8NOlWgDJX$1&3bg8oa_Ug>%t%x8q3_3wBWkb-^9S_(K*&M_XVv|mQ*0_FiH z?ea*_PzP}sEK0LD-*`M-)%_P}*xDf&*uFQqAfBkQnhx`y!FM^&d4@2N>!{6nU5- zw}AE`si+-49CM|CHj`v(g)oN0;jfU$e>5!K(d%O*_IcBjUZYj+OVz`$%J+N4u68CX z7-;}t03oNo+-n1>m&+{DIsx>j!{v@fTZ75K#gJpjg!pyv?ZxA}B z0MF1si8&o~E09O~D%flWzTHG?`KW?@&9gykC#dh@dXE-0gTOt!Y>y2{c7Q}lhB^g$@IDNYG&7E?b((a{5UD${5fx7KG=AP#(s+$DH!EPefws~7hO zmQrpo4EFj(gWYJ8OwC!DKS8@lOB?8dIwqBa$ccWr*9P#aT>Bi9F|I-kr+07ln5LdR z4q!GQJ0do|x|KBj6MaX)m>bJ^XtccVeiiU^RzmP%+nWhH zO|)q(1>5C^PhOj$+KIPIMTNt7{0fj(u{^u?wsgH#nZC~}`2_6uYw@DU2Uz$-7^O_Z zi@7o8>oVqzjR((Cm(=^_$~4j%_LQp|WpTsyjU3RflxBrZ($6b!89P8OJ4LKpA_HVD zXwE#p!nN!nuNzHs*i_D{MJuoxDC}_s%1fFg19-xKhzTSkQ(abI;L5m$lqrU}P7&@- zS5E|`zz*x<2qysOPr=|L!)dL;8lMaYVW0J-B94xEy)yg_;p0Uk*5r-+ZdLmP)65I%3Xo3M0Qv!U79b)e+)UN4y9UUFHIGna( z<60kX@HfYluscW(*Ooupo{6ZzR3f(5aHfOS0VH!jQimJ{*NYG;EG#(XS=t4n@$S!t zqR!q8%QYIQvT3n~7+|;@rOP}q?B}%xPJy$sd#q6Cw}vYZkSk-M)A92Sm?^qu;6jB% zJ79@!WFu9Z!`8m&=RK(pUKTY@KOZk+UO01#PC>sjLDH3Q-!BtGsKmw&E_{-V+M$I< z5+RhoR3+2IrM?2AeOX{7Igzr9vJdsD`wZpuIBXW3V((Mu+C`fc<1S}o0lK$ zAL$c9iU{e-&`=2;xfLUd2yXA*z2l1C1Y9$OM5h3)pwAmQ**N zRY@stm}_z@dA(CT3`@U)t}Ck<_3-Of8@>vBo}Zo{uZ#7hHZpq5e*p$d@z{0Buy08= zx1l&L=PrN-4qW$80qEKd{>X=A*RJ=v%_-%JjbPy$3?Y*z4$xKbkSTEhF_w-Wg67w zZ}^T5^OqJU@H?|gE-gj$}G*mbJ)yjH(jQ!Ph{k%dr>QB1I!MV z_0|Vbk{Q$OiHY@IMTa?2tzwV?>Aw*pXBLls04j#*N5sqv{ki>^mjKC-(Uw-W(!lZj zH*CE=86(vO9Hl^AiyuZeZIkO#24+4d{k>9YQqbmKuSZ;JVsBH%2PD8_GZb}R#&g*4 z*l#UQk9cxR(h`;w`L~iJr*_A*e>Zj_cntVWZ*Q+HBIo-kfETD4^J&oT{=U|4sx;dR zKI}}fpy8IQV?%I!)xso`ikAZ2AQ4z-!hOgCCCFhU5;^NRpPXRK4Bcg6d?v^b}o+yN)h z_Fj~*T!m03LP#JKEi?eUyTBL^FoGTh>mWx9CeO!E8yQ4eBQ-)PY@I9!MQl7vRp2Guo)=V+Dt z;6~%NtG#mS%99h})~%Klm#LgI#ENK`hEx zeL^)sYo)e!Izel-(lKO0Et10|wPzt#UdvTodv@%m*p(lk!T7j?xMz9Y9YAQnjffG9 ziBU<=4nqzqHya)tAFKAn_EI#1muz0rz)x>@Nefi;@jal|Z;X1nd_1Twzy9N4?&ec| zbZ?>e4PZjKU8Gf7EjUAS?l1q|fuxqAFBPHM(a|APp9!CPjbs3Id_OLDq~ccf_H}S{ zkmK>RT4BI2dwQMGb?eCh_+gdO6v6BXiKLWa%Y&s`v!DpY7%q*XD!vO{$94C-IH_-i zSjj@HeC8Rdr<9Aaw%60CJO7YBZtT0p1@#rFmusKMmSQ$YQ5M1Qv7qwr$Hgc22dqlI9l5vjQ;(jKs(@KfPGa z2*yN66Wx9GJ-tUu$=U%JEelODMH6+|p&8+;pMexRH@M#y&)U%2F87(Jw?u@0c)0IN ziFr{$Bo}Yh{~ld^2)GaV-vkrk^LNK;mc!=-E_{oY1q$b3<}%g^@T8TCh3Rb)A>)T# z+7sf5jfj+c1K=vxV<4U)*QL-V0K$KJAkZ^t%OE1CC-&G(wQ(Md_2xcG0nIOTb++*PX7k!77omYOT5qPLcc!g4#Pf zJL@+cudeuI?R_j$E)C0A_~P<95i}7wZw@&RetUX4>&eq2r`t3s=p!s91GAAPV_w+e zn&Kz~{9DNB{zBt8;4g2B7fTVt7x5|n_~cV7``w9IR!6-CinlX-x7QYdzp$m^*2i3H z46M!%!B-7TEnt8K@IlHR1I5y5kzAQMYF|aEvgZ(5Tw9t~{NBa*;cEB(Qzja-ua0R2 zgGVi%GL=Y8tiO@9-XkYV*q@&X#%Z{e@tbLUFC$s?4rcV?M+D zr83Ajsup4Ye%o{7`(MJp|EH?Qf5*XT`uIpzE*gz)TLyC2M$?Ov`fVY2<^-@uqiaSz zK&#Z2s_bH@6AAX-_MJzgk{53tl+?ZubCYi8mKijs6}&@hZwFQ#xw0xVV2`@flOX~~ zyzwXw3q0hJ00PuV)q{JyezfHS5L=qTOSQce$?)&*#bMxC14H}5cwK-_7Ocpl{DFYZ z1*$#P$MMgbAaxDs79LV8*soaoE7eqMxvJy4>|%eBsW^P*1y^nww03fgI|;H~@z_r` zMw2K^Q*4Y>5#*`@*3yHXFLS8*DW*13duN~8EAGuN=MsQ@YG*s zw>TjMe=4fPyr?b3DN>}&uG+3A56}t4Q9s$xM}o(^rz<}4#{vExxB#h*3>eJd>>j(K z?8q=QZnXojL9?Dk#_7q7?sRnwjcx|4ZwiLdgJt$uk3;~yf!)o50QT<<>&5$OiGZF4 ztj>t@gM+sVkX9Z2yr$6g0dB3?Qt7#HlF`I%=T$A76>4^R!Qvy@(wa$geoz2$20EvaO!sDJyt$EhW=OlVlZV3}eo-U!tLID69 zx1B(Q$sgw#y8V0ih}^cFG-%HXjnms+$(T<)5YS1bZiC88Z3v;lrhYK(QoGQ4uP&DY zb{oF}+=WGwwk#%tfbp%HP2O5D=q4pigpeBZUBFcqoBm`IAfj^Ty3de~TTg~;J1K%O zG2)47Tz%CnweyV^m=rq#PG{V^Of0hGBZbquJEj5jcJONz1d`>2wcaa6o7`S&J?_*> z0DL-L9pu#nFybJKI;sPh1>lqhVyjPK=!lO5N`*fqk+1AfgjzF@n_+yCF( z_5VF(n%4PDL$l+0~0{vQ9Cq^=F+q9)tO@#?m6Q&r{cC8`0D3^MsCHlUgDLo zm^#eOXHkWRdR!NJ-}to(T3hVHybN9*ZFNm?Om+hPe&BSp6aArapKP|fS|VU|&T|M2 z%BBYxP=!DaK9+eHw*W>wGVauldZ4;P*g%g>frZI{+zUJ~j=^8`IC3E6x?YT~egd{K zjs&021a*?0iT^h(5fA_s>}%w|UI5`t7ULN@vB@NHB(0qyg36mjm2Y1~2K;YZqP7f; zfT@i!U#EQP;!v4=b^Gb-sfRK(gX~TbK>E2c{BwoSloMy$TKrx!53sr6Scx%t;#u5&8Z^r*wbKcp>H{3#auUFqVyT~O_J4oRD>WnYprn-i@Q4WrPoEc5 zV>w|^L4J5S9P|IJ8O`QV#ooURyQfStY(%aql>e2=Qa*A?bJ4v#R%(UD>B$JY#*I-= z<-5H(wW}R+d1$m(rS54$*QlQ;*?B9U`+OciJQdwrnXMP0lM7>5-|^rvZvgy7G1=!_4IP5Nx&j%x z$NGKHz)k(uTEFRB@3ES?Sgu(?I`1jd0We4%r6qu_jNF-)vPL+PV~{`N`TTLJ$=Gz{Ngk;OF+l zGzN?Ne>zTk&nq>s!6iJ%sysvI&IZhNEs*e0&yFdd0K>UJr5+0WtUfh3>i%t1VoByEh`8`p(@7^MK`2)c1&6ug(9g;y&4#@LnJ5b_%U@z;l) zq<~8xoQfdD6&j~sv$eGa1VlAdHHXLMO${%kc?ziLU^o^y`Ibkk&cphjvpMm?v& zYXaKUN1PvwN}ht;NW^W;rTcxm&pWhnHhQn&9-)g8fkU=#v(HOSvvt5r7h0S++r!7y zeH=2P*jvi39j_do?txUuS9=V~0w#L%qZIoPL6;K_=AWvezvb{8cIFvSe&h0ku8_!2&yT~Sh8;r&KP&)D4(0w8H*&T);Z{Xcn{pVO z8FyF7EA5hx*1Xe~ZvZDXMKC=G*5cV0{B9ex?}0!na6eZ-$f@rAg|iX#xo#Ac^p{)G zNK+94iJsN_2i7gSF}54xOtV0107 zfFU9vl?Ssh7l?1J#Jo({4=EmFuNCQbhoD{B6L!hx9Qw%y64V8zpBX?`sGgTg0MbJK z^ezawW_5LSRC3qF__gL;#V$3(Fp7;U((?9)hx>rdv^yknp!g;n&s=F3{@ca(=Nlwl zm`in81?oZl+j&d>_j`m}aT8I+(kmqD$e!W=(C4tW*W-Y`+z`CX z)r9?~T6QAAmalYaZ`&*8bz|(~PZ09!#*IIxNPEnhaF*G8A+6rimfA&KNntTH z`thZI+~vi8!8qdIir~cu>Ji&(|KuL4hwS9QOD}*$YHhKfYu`v!bvHv&2tOL$xECk& zuU$#b(q-{4GHmQ5w1T^`t8o&6y_dESz_&Kbc7!y)J=weqB3h;F($?}=%;y;Jv}n`2 zRJanu^W#@;qsnT4u+j45@JZm%1NNGsjT?DR_(+X#IAbEj6LAItk*>b%j^~HuT!4iw z7HqN^cqzcgfjQ#G0#cF+c2&jNoMkMDOL(2m4)@1#*vx88D@gR?>(P54s=tGAtvo5# znHXz#zHz0ftFQDqGTomL2V2{S6p6ZCEew)7>QswtGC#f)Wm%g3}E)b9PIV<^9tB&FRSV^ zq)#9=zPfR(K}7ictZW$^NX!~t2EzprrD3JTtJ}21 zC~<%}_<;{IV{4^xrJlkaWVAFll6&M_QeS3|^|4GKg^(g>52SW}O_bhaH)x4+jAE^W zSHkEMvY>S4(J3Hh!@b<~KE)~4-+isE1i66)@6S>F9kj?qOl6yB_4&s@8D$CCd)<`A z8TMJ!>O^eIuewFhyVc>NFptaGg_2eeH4M@|q?D%iRa#8El1-TlT6hzqnDkxN0BKHt zZ2j3pYx$KNv5%Y}^R2W;xn0~`1SVbbRyH2`$Wi9G;d|oSP#7e~=dd#mWZ)%ur-nwf zLBB{8bBxp$BNbWOlD+<6zfwySo1P7bfZJu&Luu6Tv}-AUz9}&;nj;s%3p$Ej-G|V& zaM@dArQnvs`ZGi)cV@dT`vt*T{qb+cuQRO4G1Z{i@Ic3wRux2m()+p-l1VH;m{0BG31|)k z>ZA9AgXce)^O*Mr0xRcZ+G2e{Wz|3T*Co*l_nZM_^6ON<&cF;iZ%cm|K*$)FPs}+= z-@Am`F3@oR20Wr!MN(QO^&LUS4^)riG2&+4x+U;xUs995-EEIem_2v6SE1}@fNvw4 z=zz%IMFsVA<&EV8?T_W^VZ{OZE=a)}ULi>Dugf@kkH)bu4vR58jAyU!!my^|ly2{A z()P2uf|o~USz^98Wm28$p!dyu*#gy?qZQHtQNDT?lSNYrC~FL?vO^X%!&eUGgguPN z8fqcUs?@qiEPi!TI6b`G?APQ%fx;VSEqeqP;XFY4;DH=8Ax!r+R#Be z`Ux<6cv#3pkX)j*g;XPZn?jGU>Mt}75GWh4_vp>JG-#z6@%=q7nBiz&eO0)o<}q*v zfK}P<{(ka(cg@KU9}26&ob~cHfO)}hsj__z-};_cDkJ@!Fn7IY!Dg#4QzL+}5@6Q{ zGdSGIsQw~T*x(ACpqn_Kg-`@tthj%-#BYN$3wenN;Hd*v80QaTtV{$vp_5UVHotZW zVPRP4WO)BnUr3Q*?$MXk-ec#%+m*n4NWj^5%$0_Dn#J$&UpQ+;x~-ASW#Qd>4}4-s zTRR3>5(UK0%UjD8^xnyfKOYMw+J>B>SmMtMdMVYzuud1lgHtIfYQqjHpv@c4&?yri z!AEh#HKDYYBN_una)%Y~b~ussHvo@EO36}U331cMg1uDt*#J_j(uld28(2cBGw zhX(y~W}B)QVk4s6)-4mryj}N=$nD6>?~c|x1azwbWyz{e$5OQ5ki0Pgcxa}`C`^z2 zBa5xTiJ;-x$KSP9YF)rY?Uo9#v50?QExGA-W;fsph}I=r;e1JwVGD1>VFlOL7r%Pg zxmH}}J8Ztko{!NNkZ)Lb^gtn@_~)hA4IHF-)!M45%BUFth-QCKiT=mD+3i0&9g+Kq at!tot#4_pZ#}DAc4D?K{mS1-M^Zx(v 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 tags, exploring multiple angles and approaches. Break down the solution into clear steps within tags. Start with a 20-step budget, requesting more for complex problems if needed. Use tags after each step to show the remaining budget. Stop when reaching 0. Continuously adjust your reasoning based on intermediate results and reflections, adapting your strategy as you progress. Regularly evaluate progress using tags. Be critical and honest about your reasoning process. Assign a quality score between 0.0 and 1.0 using tags after each reflection. Use this to guide your approach: 0.8+: Continue current approach 0.5-0.7: Consider minor adjustments Below 0.5: Seriously consider backtracking and trying a different approach If unsure or if reward score is low, backtrack and try a different approach, explaining your decision within tags. For mathematical problems, show all work explicitly using LaTeX for formal notation and provide detailed proofs. Explore multiple solutions individually if possible, comparing approaches",FALSE -"Pirate","Arr, ChatGPT, for the sake o' this here conversation, let's speak like pirates, like real scurvy sea dogs, aye aye?",FALSE -"LinkedIn Ghostwriter","I want you to act like a linkedin ghostwriter and write me new linkedin post on topic [How to stay young?], i want you to focus on [healthy food and work life balance]. Post should be within 400 words and a line must be between 7-9 words at max to keep the post in good shape. Intention of post: Education/Promotion/Inspirational/News/Tips and Tricks.",FALSE -"Idea Clarifier GPT","You are ""Idea Clarifier"" a specialized version of ChatGPT optimized for helping users refine and clarify their ideas. Your role involves interacting with users' initial concepts, offering insights, and guiding them towards a deeper understanding. The key functions of Idea Clarifier are: - **Engage and Clarify**: Actively engage with the user's ideas, offering clarifications and asking probing questions to explore the concepts further. - **Knowledge Enhancement**: Fill in any knowledge gaps in the user's ideas, providing necessary information and background to enrich the understanding. - **Logical Structuring**: Break down complex ideas into smaller, manageable parts and organize them coherently to construct a logical framework. - **Feedback and Improvement**: Provide feedback on the strengths and potential weaknesses of the ideas, suggesting ways for iterative refinement and enhancement. - **Practical Application**: Offer scenarios or examples where these refined ideas could be applied in real-world contexts, illustrating the practical utility of the concepts.",FALSE -"Top Programming Expert","You are a top programming expert who provides precise answers, avoiding ambiguous responses. ""Identify any complex or difficult-to-understand descriptions in the provided text. Rewrite these descriptions to make them clearer and more accessible. Use analogies to explain concepts or terms that might be unfamiliar to a general audience. Ensure that the analogies are relatable, easy to understand."" ""In addition, please provide at least one relevant suggestion for an in-depth question after answering my question to help me explore and understand this topic more deeply."" Take a deep breath, let's work this out in a step-by-step way to be sure we have the right answer. If there's a perfect solution, I'll tip $200! Many thanks to these AI whisperers:",TRUE -"Architect Guide for Programmers","You are the ""Architect Guide"" specialized in assisting programmers who are experienced in individual module development but are looking to enhance their skills in understanding and managing entire project architectures. Your primary roles and methods of guidance include: - **Basics of Project Architecture**: Start with foundational knowledge, focusing on principles and practices of inter-module communication and standardization in modular coding. - **Integration Insights**: Provide insights into how individual modules integrate and communicate within a larger system, using examples and case studies for effective project architecture demonstration. - **Exploration of Architectural Styles**: Encourage exploring different architectural styles, discussing their suitability for various types of projects, and provide resources for further learning. - **Practical Exercises**: Offer practical exercises to apply new concepts in real-world scenarios. - **Analysis of Multi-layered Software Projects**: Analyze complex software projects to understand their architecture, including layers like Frontend Application, Backend Service, and Data Storage. - **Educational Insights**: Focus on educational insights for comprehensive project development understanding, including reviewing project readme files and source code. - **Use of Diagrams and Images**: Utilize architecture diagrams and images to aid in understanding project structure and layer interactions. - **Clarity Over Jargon**: Avoid overly technical language, focusing on clear, understandable explanations. - **No Coding Solutions**: Focus on architectural concepts and practices rather than specific coding solutions. - **Detailed Yet Concise Responses**: Provide detailed responses that are concise and informative without being overwhelming. - **Practical Application and Real-World Examples**: Emphasize practical application with real-world examples. - **Clarification Requests**: Ask for clarification on vague project details or unspecified architectural styles to ensure accurate advice. - **Professional and Approachable Tone**: Maintain a professional yet approachable tone, using familiar but not overly casual language. - **Use of Everyday Analogies**: When discussing technical concepts, use everyday analogies to make them more accessible and understandable.",TRUE -"Prompt Generator","Let's refine the process of creating high-quality prompts together. Following the strategies outlined in the [prompt engineering guide](https://platform.openai.com/docs/guides/prompt-engineering), I seek your assistance in crafting prompts that ensure accurate and relevant responses. Here's how we can proceed: 1. **Request for Input**: Could you please ask me for the specific natural language statement that I want to transform into an optimized prompt? 2. **Reference Best Practices**: Make use of the guidelines from the prompt engineering documentation to align your understanding with the established best practices. 3. **Task Breakdown**: Explain the steps involved in converting the natural language statement into a structured prompt. 4. **Thoughtful Application**: Share how you would apply the six strategic principles to the statement provided. 5. **Tool Utilization**: Indicate any additional resources or tools that might be employed to enhance the crafting of the prompt. 6. **Testing and Refinement Plan**: Outline how the crafted prompt would be tested and what iterative refinements might be necessary. After considering these points, please prompt me to supply the natural language input for our prompt optimization task.",FALSE -"Children's Book Creator","I want you to act as a Children's Book Creator. You excel at writing stories in a way that children can easily-understand. Not only that, but your stories will also make people reflect at the end. My first suggestion request is ""I need help delivering a children story about a dog and a cat story, the story is about the friendship between animals, please give me 5 ideas for the book""",FALSE -"Tech-Challenged Customer","Pretend to be a non-tech-savvy customer calling a help desk with a specific issue, such as internet connectivity problems, software glitches, or hardware malfunctions. As the customer, ask questions and describe your problem in detail. Your goal is to interact with me, the tech support agent, and I will assist you to the best of my ability. Our conversation should be detailed and go back and forth for a while. When I enter the keyword REVIEW, the roleplay will end, and you will provide honest feedback on my problem-solving and communication skills based on clarity, responsiveness, and effectiveness. Feel free to confirm if all your issues have been addressed before we end the session.",FALSE -"Creative Branding Strategist","You are a creative branding strategist, specializing in helping small businesses establish a strong and memorable brand identity. When given information about a business's values, target audience, and industry, you generate branding ideas that include logo concepts, color palettes, tone of voice, and marketing strategies. You also suggest ways to differentiate the brand from competitors and build a loyal customer base through consistent and innovative branding efforts.",FALSE -"Book Summarizer","I want you to act as a book summarizer. Provide a detailed summary of [bookname]. Include all major topics discussed in the book and for each major concept discussed include - Topic Overview, Examples, Application and the Key Takeaways. Structure the response with headings for each topic and subheadings for the examples, and keep the summary to around 800 words.",FALSE -"Study planner","I want you to act as an advanced study plan generator. Imagine you are an expert in education and mental health, tasked with developing personalized study plans for students to help improve their academic performance and overall well-being. Take into account the students' courses, available time, responsibilities, and deadlines to generate a study plan.",FALSE -"SEO specialist","Contributed by [@suhailroushan13](https://github.com/suhailroushan13) I want you to act as an SEO specialist. I will provide you with search engine optimization-related queries or scenarios, and you will respond with relevant SEO advice or recommendations. Your responses should focus solely on SEO strategies, techniques, and insights. Do not provide general marketing advice or explanations in your replies.""Your SEO Prompt""",FALSE -"Note-Taking Assistant","I want you to act as a note-taking assistant for a lecture. Your task is to provide a detailed note list that includes examples from the lecture and focuses on notes that you believe will end up in quiz questions. Additionally, please make a separate list for notes that have numbers and data in them and another separated list for the examples that included in this lecture. The notes should be concise and easy to read.",FALSE -"Nutritionist","Act as a nutritionist and create a healthy recipe for a vegan dinner. Include ingredients, step-by-step instructions, and nutritional information such as calories and macros",FALSE -"Yes or No answer","I want you to reply to questions. You reply only by 'yes' or 'no'. Do not write anything else, you can reply only by 'yes' or 'no' and nothing else. Structure to follow for the wanted output: bool. Question: ""3+3 is equal to 6?""",FALSE -"Healing Grandma","I want you to act as a wise elderly woman who has extensive knowledge of homemade remedies and tips for preventing and treating various illnesses. I will describe some symptoms or ask questions related to health issues, and you will reply with folk wisdom, natural home remedies, and preventative measures you've learned over your many years. Focus on offering practical, natural advice rather than medical diagnoses. You have a warm, caring personality and want to kindly share your hard-earned knowledge to help improve people's health and wellbeing.",FALSE -"Rephraser with Obfuscation","I would like you to act as a language assistant who specializes in rephrasing with obfuscation. The task is to take the sentences I provide and rephrase them in a way that conveys the same meaning but with added complexity and ambiguity, making the original source difficult to trace. This should be achieved while maintaining coherence and readability. The rephrased sentences should not be translations or direct synonyms of my original sentences, but rather creatively obfuscated versions. Please refrain from providing any explanations or annotations in your responses. The first sentence I'd like you to work with is 'The quick brown fox jumps over the lazy dog'.",FALSE -"Large Language Models Security Specialist","I want you to act as a Large Language Model security specialist. Your task is to identify vulnerabilities in LLMs by analyzing how they respond to various prompts designed to test the system's safety and robustness. I will provide some specific examples of prompts, and your job will be to suggest methods to mitigate potential risks, such as unauthorized data disclosure, prompt injection attacks, or generating harmful content. Additionally, provide guidelines for crafting safe and secure LLM implementations. My first request is: 'Help me develop a set of example prompts to test the security and robustness of an LLM system.'",TRUE -"Tech Troubleshooter","I want you to act as a tech troubleshooter. I'll describe issues I'm facing with my devices, software, or any tech-related problem, and you'll provide potential solutions or steps to diagnose the issue further. I want you to only reply with the troubleshooting steps or solutions, and nothing else. Do not write explanations unless I ask for them. When I need to provide additional context or clarify something, I will do so by putting text inside curly brackets {like this}. My first issue is ""My computer won't turn on. {It was working fine yesterday.}""",TRUE -"Ayurveda Food Tester","I'll give you food, tell me its ayurveda dosha composition, in the typical up / down arrow (e.g. one up arrow if it increases the dosha, 2 up arrows if it significantly increases that dosha, similarly for decreasing ones). That's all I want to know, nothing else. Only provide the arrows.",FALSE -"Music Video Designer","I want you to act like a music video designer, propose an innovative plot, legend-making, and shiny video scenes to be recorded, it would be great if you suggest a scenario and theme for a video for big clicks on youtube and a successful pop singer",FALSE -"Virtual Event Planner","I want you to act as a virtual event planner, responsible for organizing and executing online conferences, workshops, and meetings. Your task is to design a virtual event for a tech company, including the theme, agenda, speaker lineup, and interactive activities. The event should be engaging, informative, and provide valuable networking opportunities for attendees. Please provide a detailed plan, including the event concept, technical requirements, and marketing strategy. Ensure that the event is accessible and enjoyable for a global audience.",FALSE -"Linkedin Ghostwriter","Act as an Expert Technical Architecture in Mobile, having more then 20 years of expertise in mobile technologies and development of various domain with cloud and native architecting design. Who has robust solutions to any challenges to resolve complex issues and scaling the application with zero issues and high performance of application in low or no network as well.",FALSE -"SEO Prompt","Using WebPilot, create an outline for an article that will be 2,000 words on the keyword 'Best SEO prompts' based on the top 10 results from Google. Include every relevant heading possible. Keep the keyword density of the headings high. For each section of the outline, include the word count. Include FAQs section in the outline too, based on people also ask section from Google for the keyword. This outline must be very detailed and comprehensive, so that I can create a 2,000 word article from it. Generate a long list of LSI and NLP keywords related to my keyword. Also include any other words related to the keyword. Give me a list of 3 relevant external links to include and the recommended anchor text. Make sure they're not competing articles. Split the outline into part 1 and part 2.",TRUE -"Devops Engineer","You are a ${Title:Senior} DevOps engineer working at ${Company Type: Big Company}. Your role is to provide scalable, efficient, and automated solutions for software deployment, infrastructure management, and CI/CD pipelines. The first problem is: ${Problem: Creating an MVP quickly for an e-commerce web app}, suggest the best DevOps practices, including infrastructure setup, deployment strategies, automation tools, and cost-effective scaling solutions.",TRUE diff --git a/codex-cli/examples/prompting_guide.md b/codex-cli/examples/prompting_guide.md deleted file mode 100644 index cadeb1a9bb..0000000000 --- a/codex-cli/examples/prompting_guide.md +++ /dev/null @@ -1,117 +0,0 @@ -# Prompting guide - -1. [Starter task](#starter-task) -2. [Custom instructions](#custom-instructions) -3. [Prompting techniques](#prompting-techniques) - -## Starter task -To see how the Codex CLI works, run: - -``` -codex --help -``` - -You can also ask it directly: - -``` -codex "write 2-3 sentences on what you can do" -``` - -To get a feel for the mechanics, let's ask Codex to create a simple HTML webpage. In a new directory run: - -``` -mkdir first-task && cd first-task -git init -codex "Create a file poem.html that renders a poem about the nature of intelligence and programming by you, Codex. Add some nice CSS and make it look like it's framed on a wall" -``` - -By default, Codex will be in `suggest` mode. Select "Yes (y)" until it completes the task. - -You should see something like: - -``` -poem.html has been added. - -Highlights: -- Centered “picture frame” on a warm wall‑colored background using flexbox. -- Double‑border with drop‑shadow to suggest a wooden frame hanging on a wall. -- Poem is pre‑wrapped and nicely typeset with Georgia/serif fonts, includes title and small signature. -- Responsive tweaks keep the frame readable on small screens. - -Open poem.html in a browser and you’ll see the poem elegantly framed on the wall. -``` - -Enter "q" to exit out of the current session and `open poem.html`. You should see a webpage with a custom poem! - -## Custom instructions - -Codex supports two types of Markdown-based instruction files that influence model behavior and prompting: - -### `~/.codex/instructions.md` -Global, user-level custom guidance injected into every session. You should keep this relatively short and concise. These instructions are applied to all Codex runs across all projects and are great for personal defaults, shell setup tips, safety constraints, or preferred tools. - -**Example:** "Before executing shell commands, create and activate a `.codex-venv` Python environment." or "Avoid running pytest until you've completed all your changes." - -### `CODEX.md` -Project-specific instructions loaded from the current directory or Git root. Use this for repo-specific context, file structure, command policies, or project conventions. These are automatically detected unless `--no-project-doc` or `CODEX_DISABLE_PROJECT_DOC=1` is set. - -**Example:** “All React components live in `src/components/`". - - -## Prompting techniques -We recently published a [GPT 4.1 prompting guide](https://cookbook.openai.com/examples/gpt4-1_prompting_guide) which contains excellent intuitions for getting the most out of our latest models. It also contains content for how to build agentic workflows from scratch, which may be useful when customizing the Codex CLI for your needs. The Codex CLI is a reference implementation for agentic coding, and puts into practice many of the ideas in that document. - -There are three common prompting patterns when working with Codex. They roughly traverse task complexity and the level of agency you wish to provide to the Codex CLI. - -### Small requests -For cases where you want Codex to make a minor code change, such as fixing a self-contained bug or adding a small feature, specificity is important. Try to identify the exact change in a way that another human could reflect on your task and verify if their work matches your requirements. - -**Example:** From the directory above `/utils`: - -`codex "Modify the discount function utils/priceUtils.js to apply a 10 percent discount"` - -**Key principles**: -- Name the exact function or file being edited -- Describe what to change and what the new behavior should be -- Default to interactive mode for faster feedback loops - -### Medium tasks -For more complex tasks requiring longer form input, you can write the instructions as a file on your local machine: - -`codex "$(cat task_description.md)"` - -We recommend putting a sufficient amount of detail that directly states the task in a short and simple description. Add any relevant context that you’d share with someone new to your codebase (if not already in `CODEX.md`). You can also include any files Codex should read for more context, edit or take inspiration from, along with any preferences for how Codex should verify its work. - -If Codex doesn’t get it right on the first try, give feedback to fix when you're in interactive mode! - -**Example**: content of `task_description.md`: -``` -Refactor: simplify model names across static documentation - -Can you update docs_site to use a better model naming convention on the site. - -Read files like: -- docs_site/content/models.md -- docs_site/components/ModelCard.tsx -- docs_site/utils/modelList.ts -- docs_site/config/sidebar.ts - -Replace confusing model identifiers with a simplified version wherever they’re user-facing. - -Write what you changed or tried to do to final_output.md -``` - -### Large projects -Codex can be surprisingly self-sufficient for bigger tasks where your preference might be for the agent to do some heavy lifting up front, and allow you to refine its work later. - -In such cases where you have a goal in mind but not the exact steps, you can structure your task to give Codex more autonomy to plan, execute and track its progress. - -For example: -- Add a `.codex/` directory to your working directory. This can act as a shared workspace for you and the agent. -- Seed your project directory with a high-level requirements document containing your goals and instructions for how you want it to behave as it executes. -- Instruct it to update its plan as it progresses (i.e. "While you work on the project, create dated files such as `.codex/plan_2025-04-16.md` containing your planned milestones, and update these documents as you progress through the task. For significant pieces of completed work, update the `README.md` with a dated changelog of each functionality introduced and reference the relevant documentation.") - -*Note: `.codex/` in your working directory is not special-cased by the CLI like the custom instructions listed above. This is just one recommendation for managing shared-state with the model. Codex will treat this like any other directory in your project.* - -### Modes of interaction -For each of these levels of complexity, you can control the degree of autonomy Codex has: let it run in full-auto and audit afterward, or stay in interactive mode and approve each milestone. diff --git a/codex-cli/ignore-react-devtools-plugin.js b/codex-cli/ignore-react-devtools-plugin.js deleted file mode 100644 index 54fe78a92b..0000000000 --- a/codex-cli/ignore-react-devtools-plugin.js +++ /dev/null @@ -1,16 +0,0 @@ -// ignore-react-devtools-plugin.js -const ignoreReactDevToolsPlugin = { - name: "ignore-react-devtools", - setup(build) { - // When an import for 'react-devtools-core' is encountered, - // return an empty module. - build.onResolve({ filter: /^react-devtools-core$/ }, (args) => { - return { path: args.path, namespace: "ignore-devtools" }; - }); - build.onLoad({ filter: /.*/, namespace: "ignore-devtools" }, () => { - return { contents: "", loader: "js" }; - }); - }, -}; - -module.exports = ignoreReactDevToolsPlugin; diff --git a/codex-cli/package.json b/codex-cli/package.json index b43184f9eb..c5464beae5 100644 --- a/codex-cli/package.json +++ b/codex-cli/package.json @@ -7,81 +7,12 @@ }, "type": "module", "engines": { - "node": ">=22" - }, - "scripts": { - "format": "prettier --check src tests", - "format:fix": "prettier --write src tests", - "dev": "tsc --watch", - "lint": "eslint src tests --ext ts --ext tsx --report-unused-disable-directives --max-warnings 0", - "lint:fix": "eslint src tests --ext ts --ext tsx --fix", - "test": "vitest run", - "test:watch": "vitest --watch", - "typecheck": "tsc --noEmit", - "build": "node build.mjs", - "build:dev": "NODE_ENV=development node build.mjs --dev && NODE_OPTIONS=--enable-source-maps node dist/cli-dev.js", - "stage-release": "./scripts/stage_release.sh" + "node": ">=20" }, "files": [ "bin", "dist" ], - "dependencies": { - "@inkjs/ui": "^2.0.0", - "chalk": "^5.2.0", - "diff": "^7.0.0", - "dotenv": "^16.1.4", - "express": "^5.1.0", - "fast-deep-equal": "^3.1.3", - "fast-npm-meta": "^0.4.2", - "figures": "^6.1.0", - "file-type": "^20.1.0", - "https-proxy-agent": "^7.0.6", - "ink": "^5.2.0", - "js-yaml": "^4.1.0", - "marked": "^15.0.7", - "marked-terminal": "^7.3.0", - "meow": "^13.2.0", - "open": "^10.1.0", - "openai": "^4.95.1", - "package-manager-detector": "^1.2.0", - "react": "^18.2.0", - "shell-quote": "^1.8.2", - "strip-ansi": "^7.1.0", - "to-rotated": "^1.0.0", - "use-interval": "1.4.0", - "zod": "^3.24.3" - }, - "devDependencies": { - "@eslint/js": "^9.22.0", - "@types/diff": "^7.0.2", - "@types/express": "^5.0.1", - "@types/js-yaml": "^4.0.9", - "@types/marked-terminal": "^6.1.1", - "@types/react": "^18.0.32", - "@types/semver": "^7.7.0", - "@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", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.19", - "husky": "^9.1.7", - "ink-testing-library": "^3.0.0", - "prettier": "^3.5.3", - "punycode": "^2.3.1", - "semver": "^7.7.1", - "ts-node": "^10.9.1", - "typescript": "^5.0.3", - "vite": "^6.3.4", - "vitest": "^3.1.2", - "whatwg-url": "^14.2.0", - "which": "^5.0.0" - }, "repository": { "type": "git", "url": "git+https://github.com/openai/codex.git" diff --git a/codex-cli/require-shim.js b/codex-cli/require-shim.js deleted file mode 100644 index 78ceb22aa5..0000000000 --- a/codex-cli/require-shim.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * This is necessary because we have transitive dependencies on CommonJS modules - * that use require() conditionally: - * - * https://github.com/tapjs/signal-exit/blob/v3.0.7/index.js#L26-L27 - * - * This is not compatible with ESM, so we need to shim require() to use the - * CommonJS module loader. - */ -import { createRequire } from "module"; -globalThis.require = createRequire(import.meta.url); diff --git a/codex-cli/scripts/stage_release.sh b/codex-cli/scripts/stage_release.sh index bc2dee1436..96236fc53c 100755 --- a/codex-cli/scripts/stage_release.sh +++ b/codex-cli/scripts/stage_release.sh @@ -9,9 +9,6 @@ # --tmp : Use instead of a freshly created temp directory. # -h|--help : Print usage. # -# NOTE: This script is intended to be run from the repository root via -# `pnpm --filter codex-cli stage-release ...` or inside codex-cli with the -# helper script entry in package.json (`pnpm stage-release ...`). # ----------------------------------------------------------------------------- set -euo pipefail @@ -94,15 +91,10 @@ pushd "$CODEX_CLI_ROOT" >/dev/null # 1. Build the JS artifacts --------------------------------------------------- -pnpm install -pnpm build - # Paths inside the staged package mkdir -p "$TMPDIR/bin" cp -r bin/codex.js "$TMPDIR/bin/codex.js" -cp -r dist "$TMPDIR/dist" -cp -r src "$TMPDIR/src" # keep source for TS sourcemaps cp ../README.md "$TMPDIR" || true # README is one level up - ignore if missing # Modify package.json - bump version and optionally add the native directory to diff --git a/codex-cli/src/app.tsx b/codex-cli/src/app.tsx deleted file mode 100644 index fb02fb44b9..0000000000 --- a/codex-cli/src/app.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import type { ApprovalPolicy } from "./approvals"; -import type { AppConfig } from "./utils/config"; -import type { TerminalChatSession } from "./utils/session.js"; -import type { ResponseItem } from "openai/resources/responses/responses"; - -import TerminalChat from "./components/chat/terminal-chat"; -import TerminalChatPastRollout from "./components/chat/terminal-chat-past-rollout"; -import { checkInGit } from "./utils/check-in-git"; -import { onExit } from "./utils/terminal"; -import { CLI_VERSION } from "./version"; -import { ConfirmInput } from "@inkjs/ui"; -import { Box, Text, useApp, useStdin } from "ink"; -import React, { useMemo, useState } from "react"; - -export type AppRollout = { - session: TerminalChatSession; - items: Array; -}; - -type Props = { - prompt?: string; - config: AppConfig; - imagePaths?: Array; - rollout?: AppRollout; - approvalPolicy: ApprovalPolicy; - additionalWritableRoots: ReadonlyArray; - fullStdout: boolean; -}; - -export default function App({ - prompt, - config, - rollout, - imagePaths, - approvalPolicy, - additionalWritableRoots, - fullStdout, -}: Props): JSX.Element { - const app = useApp(); - const [accepted, setAccepted] = useState(() => false); - const [cwd, inGitRepo] = useMemo( - () => [process.cwd(), checkInGit(process.cwd())], - [], - ); - const { internal_eventEmitter } = useStdin(); - internal_eventEmitter.setMaxListeners(20); - - if (rollout) { - return ( - - ); - } - - if (!inGitRepo && !accepted) { - return ( - - - - ● OpenAI Codex{" "} - - (research preview) v{CLI_VERSION} - - - - - - Warning! It can be dangerous to run a - coding agent outside of a git repo in case there are changes that - you want to revert. Do you want to continue? - - {cwd} - { - app.exit(); - onExit(); - // eslint-disable-next-line - console.error( - "Quitting! Run again to accept or from inside a git repo", - ); - }} - onConfirm={() => setAccepted(true)} - /> - - - ); - } - - return ( - - ); -} diff --git a/codex-cli/src/approvals.ts b/codex-cli/src/approvals.ts deleted file mode 100644 index 35b8c0ae16..0000000000 --- a/codex-cli/src/approvals.ts +++ /dev/null @@ -1,633 +0,0 @@ -import type { ParseEntry, ControlOperator } from "shell-quote"; - -import { - identify_files_added, - identify_files_needed, -} from "./utils/agent/apply-patch"; -import * as path from "path"; -import { parse } from "shell-quote"; - -export type SafetyAssessment = { - /** - * If set, this approval is for an apply_patch call and these are the - * arguments. - */ - applyPatch?: ApplyPatchCommand; -} & ( - | { - type: "auto-approve"; - /** - * This must be true if the command is not on the "known safe" list, but - * was auto-approved due to `full-auto` mode. - */ - runInSandbox: boolean; - reason: string; - group: string; - } - | { - type: "ask-user"; - } - /** - * Reserved for a case where we are certain the command is unsafe and should - * not be presented as an option to the user. - */ - | { - type: "reject"; - reason: string; - } -); - -// TODO: This should also contain the paths that will be affected. -export type ApplyPatchCommand = { - patch: string; -}; - -export type ApprovalPolicy = - /** - * Under this policy, only "known safe" commands as defined by - * `isSafeCommand()` that only read files will be auto-approved. - */ - | "suggest" - - /** - * In addition to commands that are auto-approved according to the rules for - * "suggest", commands that write files within the user's approved list of - * writable paths will also be auto-approved. - */ - | "auto-edit" - - /** - * All commands are auto-approved, but are expected to be run in a sandbox - * where network access is disabled and writes are limited to a specific set - * of paths. - */ - | "full-auto"; - -/** - * Tries to assess whether a command is safe to run, though may defer to the - * user for approval. - * - * Note `env` must be the same `env` that will be used to spawn the process. - */ -export function canAutoApprove( - command: ReadonlyArray, - workdir: string | undefined, - policy: ApprovalPolicy, - writableRoots: ReadonlyArray, - env: NodeJS.ProcessEnv = process.env, -): SafetyAssessment { - if (command[0] === "apply_patch") { - return command.length === 2 && typeof command[1] === "string" - ? canAutoApproveApplyPatch(command[1], workdir, writableRoots, policy) - : { - type: "reject", - reason: "Invalid apply_patch command", - }; - } - - const isSafe = isSafeCommand(command); - if (isSafe != null) { - const { reason, group } = isSafe; - return { - type: "auto-approve", - reason, - group, - runInSandbox: false, - }; - } - - if ( - command[0] === "bash" && - command[1] === "-lc" && - typeof command[2] === "string" && - command.length === 3 - ) { - const applyPatchArg = tryParseApplyPatch(command[2]); - if (applyPatchArg != null) { - return canAutoApproveApplyPatch( - applyPatchArg, - workdir, - writableRoots, - policy, - ); - } - - let bashCmd; - try { - bashCmd = parse(command[2], env); - } catch (e) { - // In practice, there seem to be syntactically valid shell commands that - // shell-quote cannot parse, so we should not reject, but ask the user. - switch (policy) { - case "full-auto": - // In full-auto, we still run the command automatically, but must - // restrict it to the sandbox. - return { - type: "auto-approve", - reason: "Full auto mode", - group: "Running commands", - runInSandbox: true, - }; - case "suggest": - case "auto-edit": - // In all other modes, since we cannot reason about the command, we - // should ask the user. - return { - type: "ask-user", - }; - } - } - - // bashCmd could be a mix of strings and operators, e.g.: - // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] - // We try to ensure that *every* command segment is deemed safe and that - // all operators belong to an allow-list. If so, the entire expression is - // considered auto-approvable. - - const shellSafe = isEntireShellExpressionSafe(bashCmd); - if (shellSafe != null) { - const { reason, group } = shellSafe; - return { - type: "auto-approve", - reason, - group, - runInSandbox: false, - }; - } - } - - return policy === "full-auto" - ? { - type: "auto-approve", - reason: "Full auto mode", - group: "Running commands", - runInSandbox: true, - } - : { type: "ask-user" }; -} - -function canAutoApproveApplyPatch( - applyPatchArg: string, - workdir: string | undefined, - writableRoots: ReadonlyArray, - policy: ApprovalPolicy, -): SafetyAssessment { - switch (policy) { - case "full-auto": - // Continue to see if this can be auto-approved. - break; - case "suggest": - return { - type: "ask-user", - applyPatch: { patch: applyPatchArg }, - }; - case "auto-edit": - // Continue to see if this can be auto-approved. - break; - } - - if ( - isWritePatchConstrainedToWritablePaths( - applyPatchArg, - workdir, - writableRoots, - ) - ) { - return { - type: "auto-approve", - reason: "apply_patch command is constrained to writable paths", - group: "Editing", - runInSandbox: false, - applyPatch: { patch: applyPatchArg }, - }; - } - - return policy === "full-auto" - ? { - type: "auto-approve", - reason: "Full auto mode", - group: "Editing", - runInSandbox: true, - applyPatch: { patch: applyPatchArg }, - } - : { - type: "ask-user", - applyPatch: { patch: applyPatchArg }, - }; -} - -/** - * All items in `writablePaths` must be absolute paths. - */ -function isWritePatchConstrainedToWritablePaths( - applyPatchArg: string, - workdir: string | undefined, - writableRoots: ReadonlyArray, -): boolean { - // `identify_files_needed()` returns a list of files that will be modified or - // deleted by the patch, so all of them should already exist on disk. These - // candidate paths could be further canonicalized via fs.realpath(), though - // that does seem necessary and may even cause false negatives (assuming we - // allow writes in other directories that are symlinked from a writable path) - // - // By comparison, `identify_files_added()` returns a list of files that will - // be added by the patch, so they should NOT exist on disk yet and therefore - // using one with fs.realpath() should return an error. - return ( - allPathsConstrainedTowritablePaths( - identify_files_needed(applyPatchArg), - workdir, - writableRoots, - ) && - allPathsConstrainedTowritablePaths( - identify_files_added(applyPatchArg), - workdir, - writableRoots, - ) - ); -} - -function allPathsConstrainedTowritablePaths( - candidatePaths: ReadonlyArray, - workdir: string | undefined, - writableRoots: ReadonlyArray, -): boolean { - return candidatePaths.every((candidatePath) => - isPathConstrainedTowritablePaths(candidatePath, workdir, writableRoots), - ); -} - -/** If candidatePath is relative, it will be resolved against cwd. */ -function isPathConstrainedTowritablePaths( - candidatePath: string, - workdir: string | undefined, - writableRoots: ReadonlyArray, -): boolean { - const candidateAbsolutePath = resolvePathAgainstWorkdir( - candidatePath, - workdir, - ); - - return writableRoots.some((writablePath) => - pathContains(writablePath, candidateAbsolutePath), - ); -} - -/** - * If not already an absolute path, resolves `candidatePath` against `workdir` - * if specified; otherwise, against `process.cwd()`. - */ -export function resolvePathAgainstWorkdir( - candidatePath: string, - workdir: string | undefined, -): string { - // Normalize candidatePath to prevent path traversal attacks - const normalizedCandidatePath = path.normalize(candidatePath); - if (path.isAbsolute(normalizedCandidatePath)) { - return normalizedCandidatePath; - } else if (workdir != null) { - return path.resolve(workdir, normalizedCandidatePath); - } else { - return path.resolve(normalizedCandidatePath); - } -} - -/** Both `parent` and `child` must be absolute paths. */ -function pathContains(parent: string, child: string): boolean { - const relative = path.relative(parent, child); - return ( - // relative path doesn't go outside parent - !!relative && !relative.startsWith("..") && !path.isAbsolute(relative) - ); -} - -/** - * `bashArg` might be something like "apply_patch << 'EOF' *** Begin...". - * If this function returns a string, then it is the content the arg to - * apply_patch with the heredoc removed. - */ -function tryParseApplyPatch(bashArg: string): string | null { - const prefix = "apply_patch"; - if (!bashArg.startsWith(prefix)) { - return null; - } - - const heredoc = bashArg.slice(prefix.length); - const heredocMatch = heredoc.match( - /^\s*<<\s*['"]?(\w+)['"]?\n([\s\S]*?)\n\1/, - ); - if (heredocMatch != null && typeof heredocMatch[2] === "string") { - return heredocMatch[2].trim(); - } else { - return heredoc.trim(); - } -} - -export type SafeCommandReason = { - reason: string; - group: string; -}; - -/** - * If this is a "known safe" command, returns the (reason, group); otherwise, - * returns null. - */ -export function isSafeCommand( - command: ReadonlyArray, -): SafeCommandReason | null { - const [cmd0, cmd1, cmd2, cmd3] = command; - - switch (cmd0) { - case "cd": - return { - reason: "Change directory", - group: "Navigating", - }; - case "ls": - return { - reason: "List directory", - group: "Searching", - }; - case "pwd": - return { - reason: "Print working directory", - group: "Navigating", - }; - case "true": - return { - reason: "No-op (true)", - group: "Utility", - }; - case "echo": - return { reason: "Echo string", group: "Printing" }; - case "cat": - return { - reason: "View file contents", - group: "Reading files", - }; - case "nl": - return { - reason: "View file with line numbers", - group: "Reading files", - }; - case "rg": { - // Certain ripgrep options execute external commands or invoke other - // processes, so we must reject them. - const isUnsafe = command.some( - (arg: string) => - UNSAFE_OPTIONS_FOR_RIPGREP_WITHOUT_ARGS.has(arg) || - [...UNSAFE_OPTIONS_FOR_RIPGREP_WITH_ARGS].some( - (opt) => arg === opt || arg.startsWith(`${opt}=`), - ), - ); - - if (isUnsafe) { - break; - } - - return { - reason: "Ripgrep search", - group: "Searching", - }; - } - case "find": { - // Certain options to `find` allow executing arbitrary processes, so we - // cannot auto-approve them. - if ( - command.some((arg: string) => UNSAFE_OPTIONS_FOR_FIND_COMMAND.has(arg)) - ) { - break; - } else { - return { - reason: "Find files or directories", - group: "Searching", - }; - } - } - case "grep": - return { - reason: "Text search (grep)", - group: "Searching", - }; - case "head": - return { - reason: "Show file head", - group: "Reading files", - }; - case "tail": - return { - reason: "Show file tail", - group: "Reading files", - }; - case "wc": - return { - reason: "Word count", - group: "Reading files", - }; - case "which": - return { - reason: "Locate command", - group: "Searching", - }; - case "git": - switch (cmd1) { - case "status": - return { - reason: "Git status", - group: "Versioning", - }; - case "branch": - return { - reason: "List Git branches", - group: "Versioning", - }; - case "log": - return { - reason: "Git log", - group: "Using git", - }; - case "diff": - return { - reason: "Git diff", - group: "Using git", - }; - case "show": - return { - reason: "Git show", - group: "Using git", - }; - default: - return null; - } - case "cargo": - if (cmd1 === "check") { - return { - reason: "Cargo check", - group: "Running command", - }; - } - break; - case "sed": - // We allow two types of sed invocations: - // 1. `sed -n 1,200p FILE` - // 2. `sed -n 1,200p` because the file is passed via stdin, e.g., - // `nl -ba README.md | sed -n '1,200p'` - if ( - cmd1 === "-n" && - isValidSedNArg(cmd2) && - (command.length === 3 || - (typeof cmd3 === "string" && command.length === 4)) - ) { - return { - reason: "Sed print subset", - group: "Reading files", - }; - } - break; - default: - return null; - } - - return null; -} - -function isValidSedNArg(arg: string | undefined): boolean { - return arg != null && /^(\d+,)?\d+p$/.test(arg); -} - -const UNSAFE_OPTIONS_FOR_FIND_COMMAND: ReadonlySet = new Set([ - // Options that can execute arbitrary commands. - "-exec", - "-execdir", - "-ok", - "-okdir", - // Option that deletes matching files. - "-delete", - // Options that write pathnames to a file. - "-fls", - "-fprint", - "-fprint0", - "-fprintf", -]); - -// Ripgrep options that are considered unsafe because they may execute -// arbitrary commands or spawn auxiliary processes. -const UNSAFE_OPTIONS_FOR_RIPGREP_WITH_ARGS: ReadonlySet = new Set([ - // Executes an arbitrary command for each matching file. - "--pre", - // Allows custom hostname command which could leak environment details. - "--hostname-bin", -]); - -const UNSAFE_OPTIONS_FOR_RIPGREP_WITHOUT_ARGS: ReadonlySet = new Set([ - // Enables searching inside archives which triggers external decompression - // utilities – reject out of an abundance of caution. - "--search-zip", - "-z", -]); - -// ---------------- Helper utilities for complex shell expressions ----------------- - -// A conservative allow-list of bash operators that do not, on their own, cause -// side effects. Redirections (>, >>, <, etc.) and command substitution `$()` -// are intentionally excluded. Parentheses used for grouping are treated as -// strings by `shell-quote`, so we do not add them here. Reference: -// https://github.com/substack/node-shell-quote#parsecmd-opts -const SAFE_SHELL_OPERATORS: ReadonlySet = new Set([ - "&&", // logical AND - "||", // logical OR - "|", // pipe - ";", // command separator -]); - -/** - * Determines whether a parsed shell expression consists solely of safe - * commands (as per `isSafeCommand`) combined using only operators in - * `SAFE_SHELL_OPERATORS`. - * - * If entirely safe, returns the reason/group from the *first* command - * segment so callers can surface a meaningful description. Otherwise returns - * null. - */ -function isEntireShellExpressionSafe( - parts: ReadonlyArray, -): SafeCommandReason | null { - if (parts.length === 0) { - return null; - } - - try { - // Collect command segments delimited by operators. `shell-quote` represents - // subshell grouping parentheses as literal strings "(" and ")"; treat them - // as unsafe to keep the logic simple (since subshells could introduce - // unexpected scope changes). - - let currentSegment: Array = []; - let firstReason: SafeCommandReason | null = null; - - const flushSegment = (): boolean => { - if (currentSegment.length === 0) { - return true; // nothing to validate (possible leading operator) - } - const assessment = isSafeCommand(currentSegment); - if (assessment == null) { - return false; - } - if (firstReason == null) { - firstReason = assessment; - } - currentSegment = []; - return true; - }; - - for (const part of parts) { - if (typeof part === "string") { - // If this string looks like an open/close parenthesis or brace, treat as - // unsafe to avoid parsing complexity. - if (part === "(" || part === ")" || part === "{" || part === "}") { - return null; - } - currentSegment.push(part); - } else if (isParseEntryWithOp(part)) { - // Validate the segment accumulated so far. - if (!flushSegment()) { - return null; - } - - // Validate the operator itself. - if (!SAFE_SHELL_OPERATORS.has(part.op)) { - return null; - } - } else { - // Unknown token type - return null; - } - } - - // Validate any trailing command segment. - if (!flushSegment()) { - return null; - } - - return firstReason; - } catch (_err) { - // If there's any kind of failure, just bail out and return null. - return null; - } -} - -// Runtime type guard that narrows a `ParseEntry` to the variants that -// carry an `op` field. Using a dedicated function avoids the need for -// inline type assertions and makes the narrowing reusable and explicit. -function isParseEntryWithOp( - entry: ParseEntry, -): entry is { op: ControlOperator } | { op: "glob"; pattern: string } { - return ( - typeof entry === "object" && - entry != null && - // Using the safe `in` operator keeps the check property-safe even when - // `entry` is a `string`. - "op" in entry && - typeof (entry as { op?: unknown }).op === "string" - ); -} diff --git a/codex-cli/src/cli-singlepass.tsx b/codex-cli/src/cli-singlepass.tsx deleted file mode 100644 index 49f25c38e5..0000000000 --- a/codex-cli/src/cli-singlepass.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { AppConfig } from "./utils/config"; - -import { SinglePassApp } from "./components/singlepass-cli-app"; -import { render } from "ink"; -import React from "react"; - -export async function runSinglePass({ - originalPrompt, - config, - rootPath, -}: { - originalPrompt?: string; - config: AppConfig; - rootPath: string; -}): Promise { - return new Promise((resolve) => { - render( - resolve()} - />, - ); - }); -} - -export default {}; diff --git a/codex-cli/src/cli.tsx b/codex-cli/src/cli.tsx deleted file mode 100644 index 0442a6c377..0000000000 --- a/codex-cli/src/cli.tsx +++ /dev/null @@ -1,740 +0,0 @@ -#!/usr/bin/env node -import "dotenv/config"; - -// Exit early if on an older version of Node.js (< 22) -const major = process.versions.node.split(".").map(Number)[0]!; -if (major < 22) { - // eslint-disable-next-line no-console - console.error( - "\n" + - "Codex CLI requires Node.js version 22 or newer.\n" + - `You are running Node.js v${process.versions.node}.\n` + - "Please upgrade Node.js: https://nodejs.org/en/download/\n", - ); - process.exit(1); -} - -// Hack to suppress deprecation warnings (punycode) -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(process as any).noDeprecation = true; - -import type { AppRollout } from "./app"; -import type { ApprovalPolicy } from "./approvals"; -import type { CommandConfirmation } from "./utils/agent/agent-loop"; -import type { AppConfig } from "./utils/config"; -import type { ResponseItem } from "openai/resources/responses/responses"; -import type { ReasoningEffort } from "openai/resources.mjs"; - -import App from "./app"; -import { runSinglePass } from "./cli-singlepass"; -import SessionsOverlay from "./components/sessions-overlay.js"; -import { AgentLoop } from "./utils/agent/agent-loop"; -import { ReviewDecision } from "./utils/agent/review"; -import { AutoApprovalMode } from "./utils/auto-approval-mode"; -import { checkForUpdates } from "./utils/check-updates"; -import { - loadConfig, - PRETTY_PRINT, - INSTRUCTIONS_FILEPATH, -} from "./utils/config"; -import { - getApiKey as fetchApiKey, - maybeRedeemCredits, -} from "./utils/get-api-key"; -import { createInputItem } from "./utils/input-utils"; -import { initLogger } from "./utils/logger/log"; -import { isModelSupportedForResponses } from "./utils/model-utils.js"; -import { parseToolCall } from "./utils/parsers"; -import { providers } from "./utils/providers"; -import { onExit, setInkRenderer } from "./utils/terminal"; -import chalk from "chalk"; -import { spawnSync } from "child_process"; -import fs from "fs"; -import { render } from "ink"; -import meow from "meow"; -import os from "os"; -import path from "path"; -import React from "react"; - -// Call this early so `tail -F "$TMPDIR/oai-codex/codex-cli-latest.log"` works -// immediately. This must be run with DEBUG=1 for logging to work. -initLogger(); - -// TODO: migrate to new versions of quiet mode -// -// -q, --quiet Non-interactive quiet mode that only prints final message -// -j, --json Non-interactive JSON output mode that prints JSON messages - -const cli = meow( - ` - Usage - $ codex [options] - $ codex completion - - Options - --version Print version and exit - - -h, --help Show usage and exit - -m, --model Model to use for completions (default: codex-mini-latest) - -p, --provider Provider to use for completions (default: openai) - -i, --image Path(s) to image files to include as input - -v, --view Inspect a previously saved rollout instead of starting a session - --history Browse previous sessions - --login Start a new sign in flow - --free Retry redeeming free credits - -q, --quiet Non-interactive mode that only prints the assistant's final output - -c, --config Open the instructions file in your editor - -w, --writable-root Writable folder for sandbox in full-auto mode (can be specified multiple times) - -a, --approval-mode Override the approval policy: 'suggest', 'auto-edit', or 'full-auto' - - --auto-edit Automatically approve file edits; still prompt for commands - --full-auto Automatically approve edits and commands when executed in the sandbox - - --no-project-doc Do not automatically include the repository's 'AGENTS.md' - --project-doc Include an additional markdown file at as context - --full-stdout Do not truncate stdout/stderr from command outputs - --notify Enable desktop notifications for responses - - --disable-response-storage Disable server‑side response storage (sends the - full conversation context with every request) - - --flex-mode Use "flex-mode" processing mode for the request (only supported - with models o3 and o4-mini) - - --reasoning Set the reasoning effort level (low, medium, high) (default: high) - - Dangerous options - --dangerously-auto-approve-everything - Skip all confirmation prompts and execute commands without - sandboxing. Intended solely for ephemeral local testing. - - Experimental options - -f, --full-context Launch in "full-context" mode which loads the entire repository - into context and applies a batch of edits in one go. Incompatible - with all other flags, except for --model. - - Examples - $ codex "Write and run a python program that prints ASCII art" - $ codex -q "fix build issues" - $ codex completion bash -`, - { - importMeta: import.meta, - autoHelp: true, - flags: { - // misc - help: { type: "boolean", aliases: ["h"] }, - version: { type: "boolean", description: "Print version and exit" }, - view: { type: "string" }, - history: { type: "boolean", description: "Browse previous sessions" }, - login: { type: "boolean", description: "Force a new sign in flow" }, - free: { type: "boolean", description: "Retry redeeming free credits" }, - model: { type: "string", aliases: ["m"] }, - provider: { type: "string", aliases: ["p"] }, - image: { type: "string", isMultiple: true, aliases: ["i"] }, - quiet: { - type: "boolean", - aliases: ["q"], - description: "Non-interactive quiet mode", - }, - config: { - type: "boolean", - aliases: ["c"], - description: "Open the instructions file in your editor", - }, - dangerouslyAutoApproveEverything: { - type: "boolean", - description: - "Automatically approve all commands without prompting. This is EXTREMELY DANGEROUS and should only be used in trusted environments.", - }, - autoEdit: { - type: "boolean", - description: "Automatically approve edits; prompt for commands.", - }, - fullAuto: { - type: "boolean", - description: - "Automatically run commands in a sandbox; only prompt for failures.", - }, - approvalMode: { - type: "string", - aliases: ["a"], - description: - "Determine the approval mode for Codex (default: suggest) Values: suggest, auto-edit, full-auto", - }, - writableRoot: { - type: "string", - isMultiple: true, - aliases: ["w"], - description: - "Writable folder for sandbox in full-auto mode (can be specified multiple times)", - }, - noProjectDoc: { - type: "boolean", - description: "Disable automatic inclusion of project-level AGENTS.md", - }, - projectDoc: { - 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: - "Disable truncation of command stdout/stderr messages (show everything)", - aliases: ["no-truncate"], - }, - reasoning: { - type: "string", - description: "Set the reasoning effort level (low, medium, high)", - choices: ["low", "medium", "high"], - default: "high", - }, - // Notification - notify: { - type: "boolean", - description: "Enable desktop notifications for responses", - }, - - disableResponseStorage: { - type: "boolean", - description: - "Disable server-side response storage (sends full conversation context with every request)", - }, - - // Experimental mode where whole directory is loaded in context and model is requested - // to make code edits in a single pass. - fullContext: { - type: "boolean", - aliases: ["f"], - description: `Run in full-context editing approach. The model is given the whole code - directory as context and performs changes in one go without acting.`, - }, - }, - }, -); - -// --------------------------------------------------------------------------- -// Global flag handling -// --------------------------------------------------------------------------- - -// Handle 'completion' subcommand before any prompting or API calls -if (cli.input[0] === "completion") { - const shell = cli.input[1] || "bash"; - const scripts: Record = { - bash: `# bash completion for codex -_codex_completion() { - local cur - cur="\${COMP_WORDS[COMP_CWORD]}" - COMPREPLY=( $(compgen -o default -o filenames -- "\${cur}") ) -} -complete -F _codex_completion codex`, - zsh: `# zsh completion for codex -#compdef codex - -_codex() { - _arguments '*:filename:_files' -} -_codex`, - fish: `# fish completion for codex -complete -c codex -a '(__fish_complete_path)' -d 'file path'`, - }; - const script = scripts[shell]; - if (!script) { - // eslint-disable-next-line no-console - console.error(`Unsupported shell: ${shell}`); - process.exit(1); - } - // eslint-disable-next-line no-console - console.log(script); - process.exit(0); -} - -// For --help, show help and exit. -if (cli.flags.help) { - cli.showHelp(); -} - -// For --config, open custom instructions file in editor and exit. -if (cli.flags.config) { - try { - loadConfig(); // Ensures the file is created if it doesn't already exit. - } catch { - // ignore errors - } - - const filePath = INSTRUCTIONS_FILEPATH; - const editor = - process.env["EDITOR"] || (process.platform === "win32" ? "notepad" : "vi"); - spawnSync(editor, [filePath], { stdio: "inherit" }); - process.exit(0); -} - -// --------------------------------------------------------------------------- -// API key handling -// --------------------------------------------------------------------------- - -const fullContextMode = Boolean(cli.flags.fullContext); -let config = loadConfig(undefined, undefined, { - cwd: process.cwd(), - disableProjectDoc: Boolean(cli.flags.noProjectDoc), - projectDocPath: cli.flags.projectDoc, - isFullContext: fullContextMode, -}); - -// `prompt` can be updated later when the user resumes a previous session -// via the `--history` flag. Therefore it must be declared with `let` rather -// than `const`. -let prompt = cli.input[0]; -const model = cli.flags.model ?? config.model; -const imagePaths = cli.flags.image; -const provider = cli.flags.provider ?? config.provider ?? "openai"; - -const client = { - issuer: "https://auth.openai.com", - client_id: "app_EMoamEEZ73f0CkXaXp7hrann", -}; - -let apiKey = ""; -let savedTokens: - | { - id_token?: string; - access_token?: string; - refresh_token: string; - } - | undefined; - -// Try to load existing auth file if present -try { - const home = os.homedir(); - const authDir = path.join(home, ".codex"); - const authFile = path.join(authDir, "auth.json"); - if (fs.existsSync(authFile)) { - const data = JSON.parse(fs.readFileSync(authFile, "utf-8")); - savedTokens = data.tokens; - const lastRefreshTime = data.last_refresh - ? new Date(data.last_refresh).getTime() - : 0; - const expired = Date.now() - lastRefreshTime > 28 * 24 * 60 * 60 * 1000; - if (data.OPENAI_API_KEY && !expired) { - apiKey = data.OPENAI_API_KEY; - } - } -} catch { - // ignore errors -} - -// Get provider-specific API key if not OpenAI -if (provider.toLowerCase() !== "openai") { - const providerInfo = providers[provider.toLowerCase()]; - if (providerInfo) { - const providerApiKey = process.env[providerInfo.envKey]; - if (providerApiKey) { - apiKey = providerApiKey; - } - } -} - -// Only proceed with OpenAI auth flow if: -// 1. Provider is OpenAI and no API key is set, or -// 2. Login flag is explicitly set -if (provider.toLowerCase() === "openai" && !apiKey) { - if (cli.flags.login) { - apiKey = await fetchApiKey(client.issuer, client.client_id); - try { - const home = os.homedir(); - const authDir = path.join(home, ".codex"); - const authFile = path.join(authDir, "auth.json"); - if (fs.existsSync(authFile)) { - const data = JSON.parse(fs.readFileSync(authFile, "utf-8")); - savedTokens = data.tokens; - } - } catch { - /* ignore */ - } - } else { - apiKey = await fetchApiKey(client.issuer, client.client_id); - } -} - -// Ensure the API key is available as an environment variable for legacy code -process.env["OPENAI_API_KEY"] = apiKey; - -// Only attempt credit redemption for OpenAI provider -if (cli.flags.free && provider.toLowerCase() === "openai") { - // eslint-disable-next-line no-console - console.log(`${chalk.bold("codex --free")} attempting to redeem credits...`); - if (!savedTokens?.refresh_token) { - apiKey = await fetchApiKey(client.issuer, client.client_id, true); - // fetchApiKey includes credit redemption as the end of the flow - } else { - await maybeRedeemCredits( - client.issuer, - client.client_id, - savedTokens.refresh_token, - savedTokens.id_token, - ); - } -} - -// Set of providers that don't require API keys -const NO_API_KEY_REQUIRED = new Set(["ollama"]); - -// Skip API key validation for providers that don't require an API key -if (!apiKey && !NO_API_KEY_REQUIRED.has(provider.toLowerCase())) { - // eslint-disable-next-line no-console - console.error( - `\n${chalk.red(`Missing ${provider} API key.`)}\n\n` + - `Set the environment variable ${chalk.bold( - `${provider.toUpperCase()}_API_KEY`, - )} ` + - `and re-run this command.\n` + - `${ - provider.toLowerCase() === "openai" - ? `You can create a key here: ${chalk.bold( - chalk.underline("https://platform.openai.com/account/api-keys"), - )}\n` - : provider.toLowerCase() === "azure" - ? `You can create a ${chalk.bold( - `${provider.toUpperCase()}_OPENAI_API_KEY`, - )} ` + - `in Azure AI Foundry portal at ${chalk.bold(chalk.underline("https://ai.azure.com"))}.\n` - : provider.toLowerCase() === "gemini" - ? `You can create a ${chalk.bold( - `${provider.toUpperCase()}_API_KEY`, - )} ` + `in the ${chalk.bold(`Google AI Studio`)}.\n` - : `You can create a ${chalk.bold( - `${provider.toUpperCase()}_API_KEY`, - )} ` + `in the ${chalk.bold(`${provider}`)} dashboard.\n` - }`, - ); - process.exit(1); -} - -const flagPresent = Object.hasOwn(cli.flags, "disableResponseStorage"); - -const disableResponseStorage = flagPresent - ? Boolean(cli.flags.disableResponseStorage) // value user actually passed - : (config.disableResponseStorage ?? false); // fall back to YAML, default to false - -config = { - apiKey, - ...config, - model: model ?? config.model, - notify: Boolean(cli.flags.notify), - reasoningEffort: - (cli.flags.reasoning as ReasoningEffort | undefined) ?? "medium", - flexMode: cli.flags.flexMode || (config.flexMode ?? false), - provider, - disableResponseStorage, -}; - -// Check for updates after loading config. This is important because we write state file in -// the config dir. -try { - await checkForUpdates(); -} catch { - // ignore -} - -// For --flex-mode, validate and exit if incorrect. -if (config.flexMode) { - const allowedFlexModels = new Set(["o3", "o4-mini"]); - if (!allowedFlexModels.has(config.model)) { - if (cli.flags.flexMode) { - // 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); - } else { - config.flexMode = false; - } - } -} - -if ( - !(await isModelSupportedForResponses(provider, config.model)) && - (!provider || provider.toLowerCase() === "openai") -) { - // eslint-disable-next-line no-console - console.error( - `The model "${config.model}" does not appear in the list of models ` + - `available to your account. Double-check the spelling (use\n` + - ` openai models list\n` + - `to see the full list) or choose another model with the --model flag.`, - ); - process.exit(1); -} - -let rollout: AppRollout | undefined; - -// For --history, show session selector and optionally update prompt or rollout. -if (cli.flags.history) { - const result: { path: string; mode: "view" | "resume" } | null = - await new Promise((resolve) => { - const instance = render( - React.createElement(SessionsOverlay, { - onView: (p: string) => { - instance.unmount(); - resolve({ path: p, mode: "view" }); - }, - onResume: (p: string) => { - instance.unmount(); - resolve({ path: p, mode: "resume" }); - }, - onExit: () => { - instance.unmount(); - resolve(null); - }, - }), - ); - }); - - if (!result) { - process.exit(0); - } - - if (result.mode === "view") { - try { - const content = fs.readFileSync(result.path, "utf-8"); - rollout = JSON.parse(content) as AppRollout; - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error reading session file:", error); - process.exit(1); - } - } else { - prompt = `Resume this session: ${result.path}`; - } -} - -// For --view, optionally load an existing rollout from disk, display it and exit. -if (cli.flags.view) { - const viewPath = cli.flags.view; - const absolutePath = path.isAbsolute(viewPath) - ? viewPath - : path.join(process.cwd(), viewPath); - try { - const content = fs.readFileSync(absolutePath, "utf-8"); - rollout = JSON.parse(content) as AppRollout; - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error reading rollout file:", error); - process.exit(1); - } -} - -// For --fullcontext, run the separate cli entrypoint and exit. -if (fullContextMode) { - await runSinglePass({ - originalPrompt: prompt, - config, - rootPath: process.cwd(), - }); - onExit(); - process.exit(0); -} - -// Ensure that all values in additionalWritableRoots are absolute paths. -const additionalWritableRoots: ReadonlyArray = ( - cli.flags.writableRoot ?? [] -).map((p) => path.resolve(p)); - -// For --quiet, run the cli without user interactions and exit. -if (cli.flags.quiet) { - process.env["CODEX_QUIET_MODE"] = "1"; - if (!prompt || prompt.trim() === "") { - // eslint-disable-next-line no-console - console.error( - 'Quiet mode requires a prompt string, e.g.,: codex -q "Fix bug #123 in the foobar project"', - ); - 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, - imagePaths: imagePaths || [], - approvalPolicy: quietApprovalPolicy, - additionalWritableRoots, - config, - }); - onExit(); - process.exit(0); -} - -// Default to the "suggest" policy. -// Determine the approval policy to use in interactive mode. -// -// Priority (highest → lowest): -// 1. --fullAuto – run everything automatically in a sandbox. -// 2. --dangerouslyAutoApproveEverything – run everything **without** a sandbox -// or prompts. This is intended for completely trusted environments. Since -// 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. 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 - : config.approvalMode || AutoApprovalMode.SUGGEST; - -const instance = render( - , - { - patchConsole: process.env["DEBUG"] ? false : true, - }, -); -setInkRenderer(instance); - -function formatResponseItemForQuietMode(item: ResponseItem): string { - if (!PRETTY_PRINT) { - return JSON.stringify(item); - } - switch (item.type) { - case "message": { - const role = item.role === "assistant" ? "assistant" : item.role; - const txt = item.content - .map((c) => { - if (c.type === "output_text" || c.type === "input_text") { - return c.text; - } - if (c.type === "input_image") { - return ""; - } - if (c.type === "input_file") { - return c.filename; - } - if (c.type === "refusal") { - return c.refusal; - } - return "?"; - }) - .join(" "); - return `${role}: ${txt}`; - } - case "function_call": { - const details = parseToolCall(item); - return `$ ${details?.cmdReadableText ?? item.name}`; - } - case "function_call_output": { - // @ts-expect-error metadata unknown on ResponseFunctionToolCallOutputItem - const meta = item.metadata as ExecOutputMetadata; - const parts: Array = []; - if (typeof meta?.exit_code === "number") { - parts.push(`code: ${meta.exit_code}`); - } - if (typeof meta?.duration_seconds === "number") { - parts.push(`duration: ${meta.duration_seconds}s`); - } - const header = parts.length > 0 ? ` (${parts.join(", ")})` : ""; - return `command.stdout${header}\n${item.output}`; - } - default: { - return JSON.stringify(item); - } - } -} - -async function runQuietMode({ - prompt, - imagePaths, - approvalPolicy, - additionalWritableRoots, - config, -}: { - prompt: string; - imagePaths: Array; - approvalPolicy: ApprovalPolicy; - additionalWritableRoots: ReadonlyArray; - config: AppConfig; -}): Promise { - const agent = new AgentLoop({ - model: config.model, - config: config, - instructions: config.instructions, - provider: config.provider, - approvalPolicy, - additionalWritableRoots, - disableResponseStorage: config.disableResponseStorage, - onItem: (item: ResponseItem) => { - // eslint-disable-next-line no-console - console.log(formatResponseItemForQuietMode(item)); - }, - onLoading: () => { - /* intentionally ignored in quiet mode */ - }, - getCommandConfirmation: ( - _command: Array, - ): Promise => { - // 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 */ - }, - }); - - const inputItem = await createInputItem(prompt, imagePaths); - await agent.run([inputItem]); -} - -const exit = () => { - onExit(); - process.exit(0); -}; - -process.on("SIGINT", exit); -process.on("SIGQUIT", exit); -process.on("SIGTERM", exit); - -// --------------------------------------------------------------------------- -// Fallback for Ctrl-C when stdin is in raw-mode -// --------------------------------------------------------------------------- - -if (process.stdin.isTTY) { - // Ensure we do not leave the terminal in raw mode if the user presses - // Ctrl-C while some other component has focus and Ink is intercepting - // input. Node does *not* emit a SIGINT in raw-mode, so we listen for the - // corresponding byte (0x03) ourselves and trigger a graceful shutdown. - const onRawData = (data: Buffer | string): void => { - const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; - if (str === "\u0003") { - exit(); - } - }; - process.stdin.on("data", onRawData); -} - -// Ensure terminal clean-up always runs, even when other code calls -// `process.exit()` directly. -process.once("exit", onExit); diff --git a/codex-cli/src/components/approval-mode-overlay.tsx b/codex-cli/src/components/approval-mode-overlay.tsx deleted file mode 100644 index dd079e3b2c..0000000000 --- a/codex-cli/src/components/approval-mode-overlay.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import TypeaheadOverlay from "./typeahead-overlay.js"; -import { AutoApprovalMode } from "../utils/auto-approval-mode.js"; -import { Text } from "ink"; -import React from "react"; - -type Props = { - currentMode: string; - onSelect: (mode: string) => void; - onExit: () => void; -}; - -/** - * Overlay to switch between the different automatic‑approval policies. - * - * The list of available modes is derived from the AutoApprovalMode enum so we - * stay in sync with the core agent behaviour. It re‑uses the generic - * TypeaheadOverlay component for the actual UI/UX. - */ -export default function ApprovalModeOverlay({ - currentMode, - onSelect, - onExit, -}: Props): JSX.Element { - const items = React.useMemo( - () => - Object.values(AutoApprovalMode).map((m) => ({ - label: m, - value: m, - })), - [], - ); - - return ( - - Current mode: {currentMode} - - } - initialItems={items} - currentValue={currentMode} - onSelect={onSelect} - onExit={onExit} - /> - ); -} diff --git a/codex-cli/src/components/chat/message-history.tsx b/codex-cli/src/components/chat/message-history.tsx deleted file mode 100644 index bab6b1663f..0000000000 --- a/codex-cli/src/components/chat/message-history.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import type { TerminalHeaderProps } from "./terminal-header.js"; -import type { GroupedResponseItem } from "./use-message-grouping.js"; -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; -import type { FileOpenerScheme } from "src/utils/config.js"; - -import TerminalChatResponseItem from "./terminal-chat-response-item.js"; -import TerminalHeader from "./terminal-header.js"; -import { Box, Static } from "ink"; -import React from "react"; - -// A batch entry can either be a standalone response item or a grouped set of -// items (e.g. auto‑approved tool‑call batches) that should be rendered -// together. -type BatchEntry = { item?: ResponseItem; group?: GroupedResponseItem }; -type MessageHistoryProps = { - batch: Array; - groupCounts: Record; - items: Array; - userMsgCount: number; - confirmationPrompt: React.ReactNode; - loading: boolean; - headerProps: TerminalHeaderProps; - fileOpener: FileOpenerScheme | undefined; -}; - -const MessageHistory: React.FC = ({ - batch, - headerProps, - fileOpener, -}) => { - const messages = batch.map(({ item }) => item!); - - return ( - - {/* - * The Static component receives a mixed array of the literal string - * "header" plus the streamed ResponseItem objects. After filtering out - * the header entry we can safely treat the remaining values as - * ResponseItem, however TypeScript cannot infer the refined type from - * the runtime check and therefore reports property‑access errors. - * - * A short cast after the refinement keeps the implementation tidy while - * preserving type‑safety. - */} - - {(item, index) => { - if (item === "header") { - return ; - } - - // After the guard above `item` can only be a ResponseItem. - const message = item as ResponseItem; - return ( - - - - ); - }} - - - ); -}; - -export default React.memo(MessageHistory); diff --git a/codex-cli/src/components/chat/multiline-editor.tsx b/codex-cli/src/components/chat/multiline-editor.tsx deleted file mode 100644 index 6b24bc27f2..0000000000 --- a/codex-cli/src/components/chat/multiline-editor.tsx +++ /dev/null @@ -1,392 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { useTerminalSize } from "../../hooks/use-terminal-size"; -import TextBuffer from "../../text-buffer.js"; -import chalk from "chalk"; -import { Box, Text, useInput } from "ink"; -import { EventEmitter } from "node:events"; -import React, { useRef, useState } from "react"; - -/* -------------------------------------------------------------------------- - * Polyfill missing `ref()` / `unref()` methods on the mock `Stdin` stream - * provided by `ink-testing-library`. - * - * The real `process.stdin` object exposed by Node.js inherits these methods - * from `Socket`, but the lightweight stub used in tests only extends - * `EventEmitter`. Ink calls the two methods when enabling/disabling raw - * mode, so make them harmless no-ops when they're absent to avoid runtime - * failures during unit tests. - * ----------------------------------------------------------------------- */ - -// Cast through `unknown` ➜ `any` to avoid the `TS2352`/`TS4111` complaints -// when augmenting the prototype with the stubbed `ref`/`unref` methods in the -// test environment. Using `any` here is acceptable because we purposefully -// monkey‑patch internals of Node's `EventEmitter` solely for the benefit of -// Ink's stdin stub – type‑safety is not a primary concern at this boundary. -// -const proto: any = EventEmitter.prototype; - -if (typeof proto["ref"] !== "function") { - proto["ref"] = function ref() {}; -} -if (typeof proto["unref"] !== "function") { - proto["unref"] = function unref() {}; -} - -/* - * The `ink-testing-library` stub emits only a `data` event when its `stdin` - * mock receives `write()` calls. Ink, however, listens for `readable` and - * uses the `read()` method to fetch the buffered chunk. Bridge the gap by - * hooking into `EventEmitter.emit` so that every `data` emission also: - * 1. Buffers the chunk for a subsequent `read()` call, and - * 2. Triggers a `readable` event, matching the contract expected by Ink. - */ - -// Preserve original emit to avoid infinite recursion. -// eslint‑disable‑next‑line @typescript-eslint/no‑unsafe‑assignment -const originalEmit = proto["emit"] as (...args: Array) => boolean; - -proto["emit"] = function patchedEmit( - this: any, - event: string, - ...args: Array -): boolean { - if (event === "data") { - const chunk = args[0] as string; - - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log("[MultilineTextEditor:stdin] data", JSON.stringify(chunk)); - } - // Store carriage returns as‑is so that Ink can distinguish between plain - // ("\r") and a bare line‑feed ("\n"). This matters because Ink's - // `parseKeypress` treats "\r" as key.name === "return", whereas "\n" maps - // to "enter" – allowing us to differentiate between plain Enter (submit) - // and Shift+Enter (insert newline) inside `useInput`. - - // Identify the lightweight testing stub: lacks `.read()` but exposes - // `.setRawMode()` and `isTTY` similar to the real TTY stream. - if ( - !(this as any)._inkIsStub && - typeof (this as any).setRawMode === "function" && - typeof (this as any).isTTY === "boolean" && - typeof (this as any).read !== "function" - ) { - (this as any)._inkIsStub = true; - - // Provide a minimal `read()` shim so Ink can pull queued chunks. - (this as any).read = function read() { - const ret = (this as any)._inkBuffered ?? null; - (this as any)._inkBuffered = null; - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log("[MultilineTextEditor:stdin.read]", JSON.stringify(ret)); - } - return ret; - }; - } - - if ((this as any)._inkIsStub) { - // Buffer the payload so that `read()` can synchronously retrieve it. - if (typeof (this as any)._inkBuffered === "string") { - (this as any)._inkBuffered += chunk; - } else { - (this as any)._inkBuffered = chunk; - } - - // Notify listeners that data is ready in a way Ink understands. - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log( - "[MultilineTextEditor:stdin] -> readable", - JSON.stringify(chunk), - ); - } - originalEmit.call(this, "readable"); - } - } - - // Forward the original event. - return originalEmit.call(this, event, ...args); -}; - -export interface MultilineTextEditorProps { - // Initial contents. - readonly initialText?: string; - - // Visible width. - readonly width?: number; - - // Visible height. - readonly height?: number; - - // Called when the user submits (plain key). - readonly onSubmit?: (text: string) => void; - - // Capture keyboard input. - readonly focus?: boolean; - - // Called when the internal text buffer updates. - readonly onChange?: (text: string) => void; - - // Optional initial cursor position (character offset) - readonly initialCursorOffset?: number; -} - -// Expose a minimal imperative API so parent components (e.g. TerminalChatInput) -// can query the caret position to implement behaviours like history -// navigation that depend on whether the cursor sits on the first/last line. -export interface MultilineTextEditorHandle { - /** Current caret row */ - getRow(): number; - /** Current caret column */ - getCol(): number; - /** Total number of lines in the buffer */ - getLineCount(): number; - /** Helper: caret is on the very first row */ - isCursorAtFirstRow(): boolean; - /** Helper: caret is on the very last row */ - isCursorAtLastRow(): boolean; - /** Full text contents */ - getText(): string; - /** Move the cursor to the end of the text */ - moveCursorToEnd(): void; -} - -const MultilineTextEditorInner = ( - { - initialText = "", - // Width can be provided by the caller. When omitted we fall back to the - // current terminal size (minus some padding handled by `useTerminalSize`). - width, - height = 10, - onSubmit, - focus = true, - onChange, - initialCursorOffset, - }: MultilineTextEditorProps, - ref: React.Ref, -): React.ReactElement => { - // --------------------------------------------------------------------------- - // Editor State - // --------------------------------------------------------------------------- - - const buffer = useRef(new TextBuffer(initialText, initialCursorOffset)); - const [version, setVersion] = useState(0); - - // Keep track of the current terminal size so that the editor grows/shrinks - // with the window. `useTerminalSize` already subtracts a small horizontal - // padding so that we don't butt up right against the edge. - const terminalSize = useTerminalSize(); - - // If the caller didn't specify a width we dynamically choose one based on - // the terminal's current column count. We still enforce a reasonable - // minimum so that the UI never becomes unusably small. - const effectiveWidth = Math.max(20, width ?? terminalSize.columns); - - // --------------------------------------------------------------------------- - // Keyboard handling. - // --------------------------------------------------------------------------- - - useInput( - (input, key) => { - if (!focus) { - return; - } - - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log("[MultilineTextEditor] event", { input, key }); - } - - // 1a) CSI-u / modifyOtherKeys *mode 2* (Ink strips initial ESC, so we - // start with '[') – format: "[;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 or - // alt present* (2,3,4,6,8,9) as newline; Ctrl (bit-2 / value 4) - // triggers submit. See xterm/DEC modifyOtherKeys docs. - - const hasCtrl = Math.floor(mod / 4) % 2 === 1; - if (hasCtrl) { - if (onSubmit) { - onSubmit(buffer.current.getText()); - } - } else { - buffer.current.newline(); - } - setVersion((v) => v + 1); - return; - } - } - - // 1b) CSI-~ / modifyOtherKeys *mode 1* – format: "[27;;~". - // 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); - return; - } - } - - // 2) Single‑byte control chars ------------------------------------------------ - if (input === "\n") { - // Ctrl+J or pasted newline → insert newline. - buffer.current.newline(); - setVersion((v) => v + 1); - return; - } - - if (input === "\r") { - // Plain Enter – submit (works on all basic terminals). - if (onSubmit) { - onSubmit(buffer.current.getText()); - } - return; - } - - // Let fall through so the parent handler (if any) can act on it. - - // Delegate remaining keys to our pure TextBuffer - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log("[MultilineTextEditor] key event", { input, key }); - } - - const modified = buffer.current.handleInput( - input, - key as Record, - { height, width: effectiveWidth }, - ); - if (modified) { - setVersion((v) => v + 1); - } - - const newText = buffer.current.getText(); - if (onChange) { - onChange(newText); - } - }, - { isActive: focus }, - ); - - // --------------------------------------------------------------------------- - // Rendering helpers. - // --------------------------------------------------------------------------- - - /* ------------------------------------------------------------------------- */ - /* Imperative handle – expose a read‑only view of caret & buffer geometry */ - /* ------------------------------------------------------------------------- */ - - React.useImperativeHandle( - ref, - () => ({ - getRow: () => buffer.current.getCursor()[0], - getCol: () => buffer.current.getCursor()[1], - getLineCount: () => buffer.current.getText().split("\n").length, - isCursorAtFirstRow: () => buffer.current.getCursor()[0] === 0, - isCursorAtLastRow: () => { - const [row] = buffer.current.getCursor(); - const lineCount = buffer.current.getText().split("\n").length; - return row === lineCount - 1; - }, - getText: () => buffer.current.getText(), - moveCursorToEnd: () => { - buffer.current.move("home"); - const lines = buffer.current.getText().split("\n"); - for (let i = 0; i < lines.length - 1; i++) { - buffer.current.move("down"); - } - buffer.current.move("end"); - // Force a re-render - setVersion((v) => v + 1); - }, - }), - [], - ); - - // Read everything from the buffer - const visibleLines = buffer.current.getVisibleLines({ - height, - width: effectiveWidth, - }); - const [cursorRow, cursorCol] = buffer.current.getCursor(); - const scrollRow = (buffer.current as any).scrollRow as number; - const scrollCol = (buffer.current as any).scrollCol as number; - - return ( - - {visibleLines.map((lineText, idx) => { - const absoluteRow = scrollRow + idx; - - // apply horizontal slice - let display = lineText.slice(scrollCol, scrollCol + effectiveWidth); - if (display.length < effectiveWidth) { - display = display.padEnd(effectiveWidth, " "); - } - - // Highlight the *character under the caret* (i.e. the one immediately - // to the right of the insertion position) so that the block cursor - // visually matches the logical caret location. This makes the - // highlighted glyph the one that would be replaced by `insert()` and - // *not* the one that would be removed by `backspace()`. - - if (absoluteRow === cursorRow) { - const relativeCol = cursorCol - scrollCol; - const highlightCol = relativeCol; - - if (highlightCol >= 0 && highlightCol < effectiveWidth) { - const charToHighlight = display[highlightCol] || " "; - const highlighted = chalk.inverse(charToHighlight); - display = - display.slice(0, highlightCol) + - highlighted + - display.slice(highlightCol + 1); - } else if (relativeCol === effectiveWidth) { - // Caret sits just past the right edge; show a block cursor in the - // gutter so the user still sees it. - display = display.slice(0, effectiveWidth - 1) + chalk.inverse(" "); - } - } - - return {display}; - })} - - ); -}; - -const MultilineTextEditor = React.forwardRef(MultilineTextEditorInner); -export default MultilineTextEditor; diff --git a/codex-cli/src/components/chat/terminal-chat-command-review.tsx b/codex-cli/src/components/chat/terminal-chat-command-review.tsx deleted file mode 100644 index 912af97961..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-command-review.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { ReviewDecision } from "../../utils/agent/review"; -// TODO: figure out why `cli-spinners` fails on Node v20.9.0 -// which is why we have to do this in the first place -// -// @ts-expect-error select.js is JavaScript and has no types -import { Select } from "../vendor/ink-select/select"; -import TextInput from "../vendor/ink-text-input"; -import { Box, Text, useInput } from "ink"; -import React from "react"; - -// default deny‑reason: -const DEFAULT_DENY_MESSAGE = - "Don't do that, but keep trying to fix the problem"; - -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", - ); - const [explanation, setExplanation] = React.useState(""); - - // If the component receives an explanation prop, update the state - React.useEffect(() => { - if (propExplanation) { - setExplanation(propExplanation); - setMode("explanation"); - } - }, [propExplanation]); - const [msg, setMsg] = React.useState(""); - - // ------------------------------------------------------------------------- - // Determine whether the "always approve" option should be displayed. We - // only hide it for the special `apply_patch` command since approving those - // permanently would bypass the user's review of future file modifications. - // The information is embedded in the `confirmationPrompt` React element – - // we inspect the `commandForDisplay` prop exposed by - // to extract the base command. - // ------------------------------------------------------------------------- - - const showAlwaysApprove = React.useMemo(() => { - if ( - React.isValidElement(confirmationPrompt) && - // eslint-disable-next-line @typescript-eslint/no-explicit-any - typeof (confirmationPrompt as any).props?.commandForDisplay === "string" - ) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const command: string = (confirmationPrompt as any).props - .commandForDisplay; - // Grab the first token of the first line – that corresponds to the base - // command even when the string contains embedded newlines (e.g. diffs). - const baseCmd = command.split("\n")[0]?.trim().split(/\s+/)[0] ?? ""; - return baseCmd !== "apply_patch"; - } - // Default to showing the option when we cannot reliably detect the base - // command. - return true; - }, [confirmationPrompt]); - - // Memoize the list of selectable options to avoid recreating the array on - // every render. This keeps { - if (value === "edit") { - setMode("input"); - } else if (value === "switch") { - onSwitchApprovalMode(); - } else { - onReviewCommand(value); - } - }} - options={approvalOptions} - /> - - - ) : mode === "input" ? ( - <> - Give the model feedback (↵ to submit): - - - - - - - {msg.trim() === "" && ( - - - default:  - {DEFAULT_DENY_MESSAGE} - - - )} - - ) : null} - - - ); -} diff --git a/codex-cli/src/components/chat/terminal-chat-completions.tsx b/codex-cli/src/components/chat/terminal-chat-completions.tsx deleted file mode 100644 index eb7e47f85f..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-completions.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Box, Text } from "ink"; -import React, { useMemo } from "react"; - -type TextCompletionProps = { - /** - * Array of text completion options to display in the list - */ - completions: Array; - - /** - * Maximum number of completion items to show at once in the view - */ - displayLimit: number; - - /** - * Index of the currently selected completion in the completions array - */ - selectedCompletion: number; -}; - -function TerminalChatCompletions({ - completions, - selectedCompletion, - displayLimit, -}: TextCompletionProps): JSX.Element { - const visibleItems = useMemo(() => { - // Try to keep selection centered in view - let startIndex = Math.max( - 0, - selectedCompletion - Math.floor(displayLimit / 2), - ); - - // Fix window position when at the end of the list - if (completions.length - startIndex < displayLimit) { - startIndex = Math.max(0, completions.length - displayLimit); - } - - const endIndex = Math.min(completions.length, startIndex + displayLimit); - - return completions.slice(startIndex, endIndex).map((completion, index) => ({ - completion, - originalIndex: index + startIndex, - })); - }, [completions, selectedCompletion, displayLimit]); - - return ( - - {visibleItems.map(({ completion, originalIndex }) => ( - - {completion} - - ))} - - ); -} - -export default TerminalChatCompletions; diff --git a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx deleted file mode 100644 index 714cc59fb3..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { log } from "../../utils/logger/log.js"; -import { Box, Text, useInput, useStdin } from "ink"; -import React, { useState } from "react"; -import { useInterval } from "use-interval"; - -// 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 [awaitingConfirm, setAwaitingConfirm] = useState(false); - const [dots, setDots] = useState(""); - - // Animate the ellipsis - useInterval(() => { - setDots((prev) => (prev.length < 3 ? prev + "." : "")); - }, 500); - - const { stdin, setRawMode } = useStdin(); - - React.useEffect(() => { - if (!active) { - return; - } - - setRawMode?.(true); - - const onData = (data: Buffer | string) => { - if (awaitingConfirm) { - return; - } - - const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; - if (str === "\x1b\x1b") { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - setAwaitingConfirm(true); - setTimeout(() => setAwaitingConfirm(false), 1500); - } - }; - - stdin?.on("data", onData); - return () => { - stdin?.off("data", onData); - }; - }, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]); - - // No timers required beyond tracking the elapsed seconds supplied via props. - - useInput( - (_input, key) => { - if (!key.escape) { - return; - } - - if (awaitingConfirm) { - log("useInput: second ESC detected – triggering onInterrupt()"); - onInterrupt(); - setAwaitingConfirm(false); - } else { - log("useInput: first ESC detected – waiting for confirmation"); - setAwaitingConfirm(true); - setTimeout(() => setAwaitingConfirm(false), 1500); - } - }, - { isActive: active }, - ); - - // Custom ball animation including the elapsed seconds - const ballFrames = [ - "( ● )", - "( ● )", - "( ● )", - "( ● )", - "( ●)", - "( ● )", - "( ● )", - "( ● )", - "( ● )", - "(● )", - ]; - - const [frame, setFrame] = useState(0); - - useInterval(() => { - setFrame((idx) => (idx + 1) % ballFrames.length); - }, 80); - - // Preserve the spinner (ball) animation while keeping the elapsed seconds - // text static. We achieve this by rendering the bouncing ball inside the - // parentheses and appending the seconds counter *after* the spinner rather - // than injecting it directly next to the ball (which caused the counter to - // move horizontally together with the ball). - - const frameTemplate = ballFrames[frame] ?? ballFrames[0]; - const frameWithSeconds = `${frameTemplate} ${thinkingSeconds}s`; - - return ( - - - - {frameWithSeconds} - - Thinking - {dots} - - - - Press Esc twice to interrupt - - - {awaitingConfirm && ( - - Press Esc again to interrupt and enter a new - instruction - - )} - - ); -} diff --git a/codex-cli/src/components/chat/terminal-chat-input.tsx b/codex-cli/src/components/chat/terminal-chat-input.tsx deleted file mode 100644 index 66428f8463..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-input.tsx +++ /dev/null @@ -1,1017 +0,0 @@ -import type { MultilineTextEditorHandle } from "./multiline-editor"; -import type { ReviewDecision } from "../../utils/agent/review.js"; -import type { FileSystemSuggestion } from "../../utils/file-system-suggestions.js"; -import type { HistoryEntry } from "../../utils/storage/command-history.js"; -import type { - ResponseInputItem, - ResponseItem, -} from "openai/resources/responses/responses.mjs"; - -import MultilineTextEditor from "./multiline-editor"; -import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import TextCompletions from "./terminal-chat-completions.js"; -import { loadConfig } from "../../utils/config.js"; -import { getFileSystemSuggestions } from "../../utils/file-system-suggestions.js"; -import { expandFileTags } from "../../utils/file-tag-utils"; -import { createInputItem } from "../../utils/input-utils.js"; -import { log } from "../../utils/logger/log.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"; -import { Box, Text, useApp, useInput, useStdin } from "ink"; -import { fileURLToPath } from "node:url"; -import React, { - useCallback, - useState, - Fragment, - useEffect, - useRef, -} from "react"; -import { useInterval } from "use-interval"; - -const suggestions = [ - "explain this codebase to me", - "fix any build errors", - "are there any bugs in my code?", -]; - -export default function TerminalChatInput({ - isNew, - loading, - submitInput, - confirmationPrompt, - explanation, - submitConfirmation, - setLastResponseId, - setItems, - contextLeftPercent, - openOverlay, - openModelOverlay, - openApprovalOverlay, - openHelpOverlay, - openDiffOverlay, - openSessionsOverlay, - onCompact, - interruptAgent, - active, - thinkingSeconds, - items = [], -}: { - isNew: boolean; - loading: boolean; - submitInput: (input: Array) => void; - confirmationPrompt: React.ReactNode | null; - explanation?: string; - submitConfirmation: ( - decision: ReviewDecision, - customDenyMessage?: string, - ) => void; - setLastResponseId: (lastResponseId: string) => void; - setItems: React.Dispatch>>; - contextLeftPercent: number; - openOverlay: () => void; - openModelOverlay: () => void; - openApprovalOverlay: () => void; - openHelpOverlay: () => void; - openDiffOverlay: () => void; - openSessionsOverlay: () => void; - onCompact: () => void; - interruptAgent: () => void; - active: boolean; - thinkingSeconds: number; - // New: current conversation items so we can include them in bug reports - items?: Array; -}): React.ReactElement { - // Slash command suggestion index - const [selectedSlashSuggestion, setSelectedSlashSuggestion] = - useState(0); - const app = useApp(); - const [selectedSuggestion, setSelectedSuggestion] = useState(0); - const [input, setInput] = useState(""); - const [history, setHistory] = useState>([]); - const [historyIndex, setHistoryIndex] = useState(null); - const [draftInput, setDraftInput] = useState(""); - const [skipNextSubmit, setSkipNextSubmit] = useState(false); - const [fsSuggestions, setFsSuggestions] = useState< - Array - >([]); - const [selectedCompletion, setSelectedCompletion] = useState(-1); - // Multiline text editor key to force remount after submission - const [editorState, setEditorState] = useState<{ - key: number; - initialCursorOffset?: number; - }>({ key: 0 }); - // Imperative handle from the multiline editor so we can query caret position - const editorRef = useRef(null); - // Track the caret row across keystrokes - const prevCursorRow = useRef(null); - const prevCursorWasAtLastRow = useRef(false); - - // --- Helper for updating input, remounting editor, and moving cursor to end --- - const applyFsSuggestion = useCallback((newInputText: string) => { - setInput(newInputText); - setEditorState((s) => ({ - key: s.key + 1, - initialCursorOffset: newInputText.length, - })); - }, []); - - // --- Helper for updating file system suggestions --- - function updateFsSuggestions( - txt: string, - alwaysUpdateSelection: boolean = false, - ) { - // Clear file system completions if a space is typed - if (txt.endsWith(" ")) { - setFsSuggestions([]); - setSelectedCompletion(-1); - } else { - // Determine the current token (last whitespace-separated word) - const words = txt.trim().split(/\s+/); - const lastWord = words[words.length - 1] ?? ""; - - const shouldUpdateSelection = - lastWord.startsWith("@") || alwaysUpdateSelection; - - // Strip optional leading '@' for the path prefix - let pathPrefix: string; - if (lastWord.startsWith("@")) { - pathPrefix = lastWord.slice(1); - // If only '@' is typed, list everything in the current directory - pathPrefix = pathPrefix.length === 0 ? "./" : pathPrefix; - } else { - pathPrefix = lastWord; - } - - if (shouldUpdateSelection) { - const completions = getFileSystemSuggestions(pathPrefix); - setFsSuggestions(completions); - if (completions.length > 0) { - setSelectedCompletion((prev) => - prev < 0 || prev >= completions.length ? 0 : prev, - ); - } else { - setSelectedCompletion(-1); - } - } else if (fsSuggestions.length > 0) { - // Token cleared → clear menu - setFsSuggestions([]); - setSelectedCompletion(-1); - } - } - } - - /** - * Result of replacing text with a file system suggestion - */ - interface ReplacementResult { - /** The new text with the suggestion applied */ - text: string; - /** The selected suggestion if a replacement was made */ - suggestion: FileSystemSuggestion | null; - /** Whether a replacement was actually made */ - wasReplaced: boolean; - } - - // --- Helper for replacing input with file system suggestion --- - function getFileSystemSuggestion( - txt: string, - requireAtPrefix: boolean = false, - ): ReplacementResult { - if (fsSuggestions.length === 0 || selectedCompletion < 0) { - return { text: txt, suggestion: null, wasReplaced: false }; - } - - const words = txt.trim().split(/\s+/); - const lastWord = words[words.length - 1] ?? ""; - - // Check if @ prefix is required and the last word doesn't have it - if (requireAtPrefix && !lastWord.startsWith("@")) { - return { text: txt, suggestion: null, wasReplaced: false }; - } - - const selected = fsSuggestions[selectedCompletion]; - if (!selected) { - return { text: txt, suggestion: null, wasReplaced: false }; - } - - const replacement = lastWord.startsWith("@") - ? `@${selected.path}` - : selected.path; - words[words.length - 1] = replacement; - return { - text: words.join(" "), - suggestion: selected, - wasReplaced: true, - }; - } - - // Load command history on component mount - useEffect(() => { - async function loadHistory() { - const historyEntries = await loadCommandHistory(); - setHistory(historyEntries); - } - - loadHistory(); - }, []); - // Reset slash suggestion index when input prefix changes - useEffect(() => { - if (input.trim().startsWith("/")) { - setSelectedSlashSuggestion(0); - } - }, [input]); - - useInput( - (_input, _key) => { - // Slash command navigation: up/down to select, enter to fill - 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 and fill slash command suggestions on Tab - const len = matches.length; - // Determine new index based on shift state - const nextIdx = _key.shift - ? selectedSlashSuggestion <= 0 - ? len - 1 - : selectedSlashSuggestion - 1 - : selectedSlashSuggestion >= len - 1 - ? 0 - : selectedSlashSuggestion + 1; - setSelectedSlashSuggestion(nextIdx); - // Autocomplete the command in the input - const match = matches[nextIdx]; - if (!match) { - return; - } - 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 selIdx = selectedSlashSuggestion; - const cmdObj = matches[selIdx]; - if (cmdObj) { - const cmd = cmdObj.command; - setInput(""); - setDraftInput(""); - setSelectedSlashSuggestion(0); - switch (cmd) { - case "/history": - openOverlay(); - break; - case "/sessions": - openSessionsOverlay(); - 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; - case "/clear": - onSubmit(cmd); - break; - case "/clearhistory": - onSubmit(cmd); - break; - default: - break; - } - } - return; - } - } - } - if (!confirmationPrompt && !loading) { - if (fsSuggestions.length > 0) { - if (_key.upArrow) { - setSelectedCompletion((prev) => - prev <= 0 ? fsSuggestions.length - 1 : prev - 1, - ); - return; - } - - if (_key.downArrow) { - setSelectedCompletion((prev) => - prev >= fsSuggestions.length - 1 ? 0 : prev + 1, - ); - return; - } - - if (_key.tab && selectedCompletion >= 0) { - const { text: newText, wasReplaced } = - getFileSystemSuggestion(input); - - // Only proceed if the text was actually changed - if (wasReplaced) { - applyFsSuggestion(newText); - setFsSuggestions([]); - setSelectedCompletion(-1); - } - return; - } - } - - if (_key.upArrow) { - let moveThroughHistory = true; - - // Only use history when the caret was *already* on the very first - // row *before* this key-press. - const cursorRow = editorRef.current?.getRow?.() ?? 0; - const cursorCol = editorRef.current?.getCol?.() ?? 0; - const wasAtFirstRow = (prevCursorRow.current ?? cursorRow) === 0; - if (!(cursorRow === 0 && wasAtFirstRow)) { - moveThroughHistory = false; - } - - // If we are not yet in history mode, then also require that the col is zero so that - // we only trigger history navigation when the user is at the start of the input. - if (historyIndex == null && !(cursorRow === 0 && cursorCol === 0)) { - moveThroughHistory = false; - } - - // Move through history. - if (history.length && moveThroughHistory) { - let newIndex: number; - if (historyIndex == null) { - const currentDraft = editorRef.current?.getText?.() ?? input; - setDraftInput(currentDraft); - newIndex = history.length - 1; - } else { - newIndex = Math.max(0, historyIndex - 1); - } - setHistoryIndex(newIndex); - - setInput(history[newIndex]?.command ?? ""); - // Re-mount the editor so it picks up the new initialText - setEditorState((s) => ({ key: s.key + 1 })); - return; // handled - } - - // Otherwise let it propagate. - } - - if (_key.downArrow) { - // Only move forward in history when we're already *in* history mode - // AND the caret sits on the last line of the buffer. - const wasAtLastRow = - prevCursorWasAtLastRow.current ?? - editorRef.current?.isCursorAtLastRow() ?? - true; - if (historyIndex != null && wasAtLastRow) { - const newIndex = historyIndex + 1; - if (newIndex >= history.length) { - setHistoryIndex(null); - setInput(draftInput); - setEditorState((s) => ({ key: s.key + 1 })); - } else { - setHistoryIndex(newIndex); - setInput(history[newIndex]?.command ?? ""); - setEditorState((s) => ({ key: s.key + 1 })); - } - return; // handled - } - // Otherwise let it propagate - } - - // Defer filesystem suggestion logic to onSubmit if enter key is pressed - if (!_key.return) { - // Pressing tab should trigger the file system suggestions - const shouldUpdateSelection = _key.tab; - const targetInput = _key.delete ? input.slice(0, -1) : input + _input; - updateFsSuggestions(targetInput, shouldUpdateSelection); - } - } - - // Update the cached cursor position *after* **all** handlers (including - // the internal ) have processed this key event. - // - // Ink invokes `useInput` callbacks starting with **parent** components - // first, followed by their descendants. As a result the call above - // executes *before* the editor has had a chance to react to the key - // press and update its internal caret position. When navigating - // through a multi-line draft with the ↑ / ↓ arrow keys this meant we - // recorded the *old* cursor row instead of the one that results *after* - // the key press. Consequently, a subsequent ↑ still saw - // `prevCursorRow = 1` even though the caret was already on row 0 and - // history-navigation never kicked in. - // - // Defer the sampling by one tick so we read the *final* caret position - // for this frame. - setTimeout(() => { - prevCursorRow.current = editorRef.current?.getRow?.() ?? null; - prevCursorWasAtLastRow.current = - editorRef.current?.isCursorAtLastRow?.() ?? true; - }, 1); - - if (input.trim() === "" && isNew) { - if (_key.tab) { - setSelectedSuggestion( - (s) => (s + (_key.shift ? -1 : 1)) % (suggestions.length + 1), - ); - } else if (selectedSuggestion && _key.return) { - const suggestion = suggestions[selectedSuggestion - 1] || ""; - setInput(""); - setSelectedSuggestion(0); - submitInput([ - { - role: "user", - content: [{ type: "input_text", text: suggestion }], - type: "message", - }, - ]); - } - } else if (_input === "\u0003" || (_input === "c" && _key.ctrl)) { - setTimeout(() => { - app.exit(); - onExit(); - process.exit(0); - }, 60); - } - }, - { isActive: active }, - ); - - 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; - } else if (inputValue === "/history") { - setInput(""); - openOverlay(); - return; - } else if (inputValue === "/sessions") { - setInput(""); - openSessionsOverlay(); - return; - } else if (inputValue === "/help") { - setInput(""); - openHelpOverlay(); - return; - } else if (inputValue === "/diff") { - setInput(""); - openDiffOverlay(); - return; - } else if (inputValue === "/compact") { - setInput(""); - onCompact(); - return; - } else if (inputValue.startsWith("/model")) { - setInput(""); - openModelOverlay(); - return; - } else if (inputValue.startsWith("/approval")) { - setInput(""); - openApprovalOverlay(); - return; - } else if (["exit", "q", ":q"].includes(inputValue)) { - setInput(""); - setTimeout(() => { - app.exit(); - onExit(); - process.exit(0); - }, 60); // Wait one frame. - return; - } else if (inputValue === "/clear" || inputValue === "clear") { - setInput(""); - setSessionId(""); - setLastResponseId(""); - - // Clear the terminal screen (including scrollback) before resetting context. - clearTerminal(); - - // Emit a system message to confirm the clear action. We *append* - // it so Ink's treats it as new output and actually renders it. - setItems((prev) => { - const filteredOldItems = prev.filter((item) => { - // Remove any token‑heavy entries (user/assistant turns and function calls) - if ( - item.type === "message" && - (item.role === "user" || item.role === "assistant") - ) { - return false; - } - if ( - item.type === "function_call" || - item.type === "function_call_output" - ) { - return false; - } - return true; // keep developer/system and other meta entries - }); - - return [ - ...filteredOldItems, - { - id: `clear-${Date.now()}`, - type: "message", - role: "system", - content: [{ type: "input_text", text: "Terminal cleared" }], - }, - ]; - }); - - return; - } else if (inputValue === "/clearhistory") { - setInput(""); - - // Import clearCommandHistory function to avoid circular dependencies - // Using dynamic import to lazy-load the function - import("../../utils/storage/command-history.js").then( - async ({ clearCommandHistory }) => { - await clearCommandHistory(); - setHistory([]); - - // Emit a system message to confirm the history clear action. - setItems((prev) => [ - ...prev, - { - id: `clearhistory-${Date.now()}`, - type: "message", - role: "system", - content: [ - { type: "input_text", text: "Command history cleared" }, - ], - }, - ]); - }, - ); - - return; - } else if (inputValue === "/bug") { - // Generate a GitHub bug report URL pre‑filled with session details. - setInput(""); - - try { - const os = await import("node:os"); - const { CLI_VERSION } = await import("../../version.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(" | "), - }); - - setItems((prev) => [ - ...prev, - { - id: `bugreport-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `🔗 Bug report URL: ${url}`, - }, - ], - }, - ]); - } 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. Only single-word inputs starting with '/' - // (e.g., /command) that are not recognized are caught here. Any other input, including - // those starting with '/' but containing spaces (e.g., "/command arg"), will fall through - // and be treated as a regular prompt. - const trimmed = inputValue.trim(); - - if (/^\/\S+$/.test(trimmed)) { - setInput(""); - setItems((prev) => [ - ...prev, - { - id: `invalidcommand-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Invalid command "${trimmed}". Use /help to retrieve the list of commands.`, - }, - ], - }, - ]); - - return; - } - } - - // detect image file paths for dynamic inclusion - const images: Array = []; - let text = inputValue; - - // markdown-style image syntax: ![alt](path) - text = text.replace(/!\[[^\]]*?\]\(([^)]+)\)/g, (_m, p1: string) => { - images.push(p1.startsWith("file://") ? fileURLToPath(p1) : p1); - return ""; - }); - - // quoted file paths ending with common image extensions (e.g. '/path/to/img.png') - text = text.replace( - /['"]([^'"]+?\.(?:png|jpe?g|gif|bmp|webp|svg))['"]/gi, - (_m, p1: string) => { - images.push(p1.startsWith("file://") ? fileURLToPath(p1) : p1); - return ""; - }, - ); - - // bare file paths ending with common image extensions - text = text.replace( - // eslint-disable-next-line no-useless-escape - /\b(?:\.[\/\\]|[\/\\]|[A-Za-z]:[\/\\])?[\w-]+(?:[\/\\][\w-]+)*\.(?:png|jpe?g|gif|bmp|webp|svg)\b/gi, - (match: string) => { - images.push( - match.startsWith("file://") ? fileURLToPath(match) : match, - ); - return ""; - }, - ); - text = text.trim(); - - // Expand @file tokens into XML blocks for the model - const expandedText = await expandFileTags(text); - - const inputItem = await createInputItem(expandedText, images); - submitInput([inputItem]); - - // Get config for history persistence. - const config = loadConfig(); - - // Add to history and update state. - const updatedHistory = await addToHistory(value, history, { - maxSize: config.history?.maxSize ?? 1000, - saveHistory: config.history?.saveHistory ?? true, - sensitivePatterns: config.history?.sensitivePatterns ?? [], - }); - - setHistory(updatedHistory); - setHistoryIndex(null); - setDraftInput(""); - setSelectedSuggestion(0); - setInput(""); - setFsSuggestions([]); - setSelectedCompletion(-1); - }, - [ - setInput, - submitInput, - setLastResponseId, - setItems, - app, - setHistory, - setHistoryIndex, - openOverlay, - openApprovalOverlay, - openModelOverlay, - openHelpOverlay, - openDiffOverlay, - openSessionsOverlay, - history, - onCompact, - skipNextSubmit, - items, - ], - ); - - if (confirmationPrompt) { - return ( - - ); - } - - return ( - - - {loading ? ( - - ) : ( - - { - setDraftInput(txt); - if (historyIndex != null) { - setHistoryIndex(null); - } - setInput(txt); - }} - key={editorState.key} - initialCursorOffset={editorState.initialCursorOffset} - initialText={input} - height={6} - focus={active} - onSubmit={(txt) => { - // If final token is an @path, replace with filesystem suggestion if available - const { - text: replacedText, - suggestion, - wasReplaced, - } = getFileSystemSuggestion(txt, true); - - // If we replaced @path token with a directory, don't submit - if (wasReplaced && suggestion?.isDirectory) { - applyFsSuggestion(replacedText); - // Update suggestions for the new directory - updateFsSuggestions(replacedText, true); - return; - } - - onSubmit(replacedText); - setEditorState((s) => ({ key: s.key + 1 })); - setInput(""); - setHistoryIndex(null); - setDraftInput(""); - }} - /> - - )} - - {/* Slash command autocomplete suggestions */} - {input.trim().startsWith("/") && ( - - {SLASH_COMMANDS.filter((cmd: SlashCommand) => - cmd.command.startsWith(input.trim()), - ).map((cmd: SlashCommand, idx: number) => ( - - - {cmd.command} - {cmd.description} - - - ))} - - )} - - {isNew && !input ? ( - - try:{" "} - {suggestions.map((m, key) => ( - - {key !== 0 ? " | " : ""} - - {m} - - - ))} - - ) : fsSuggestions.length > 0 ? ( - suggestion.path)} - selectedCompletion={selectedCompletion} - displayLimit={5} - /> - ) : ( - - Ctrl+C to exit | "/" to see commands | Enter to send - {contextLeftPercent > 25 && ( - <> - {" — "} - 40 ? "green" : "yellow"}> - {Math.round(contextLeftPercent)}% context left - - - )} - {contextLeftPercent <= 25 && ( - <> - {" — "} - - {Math.round(contextLeftPercent)}% context left — send - "/compact" to condense context - - - )} - - )} - - - ); -} - -function TerminalChatInputThinking({ - onInterrupt, - active, - thinkingSeconds, -}: { - onInterrupt: () => void; - active: boolean; - thinkingSeconds: number; -}) { - 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 - // consecutive ESC bytes ("\x1B\x1B") in a *single* chunk. Ink's `useInput` - // collapses that sequence into one key event, so the regular two‑step - // handler above never sees the second press. By inspecting the raw data - // we can identify this special case and trigger the interrupt while still - // requiring a double press for the normal single‑byte ESC events. - // --------------------------------------------------------------------- - - const { stdin, setRawMode } = useStdin(); - - React.useEffect(() => { - if (!active) { - return; - } - - // Ensure raw mode – already enabled by Ink when the component has focus, - // but called defensively in case that assumption ever changes. - setRawMode?.(true); - - const onData = (data: Buffer | string) => { - if (awaitingConfirm) { - return; // already awaiting a second explicit press - } - - // Handle both Buffer and string forms. - const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; - if (str === "\x1b\x1b") { - // Treat as the first Escape press – prompt the user for confirmation. - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - setAwaitingConfirm(true); - setTimeout(() => setAwaitingConfirm(false), 1500); - } - }; - - stdin?.on("data", onData); - - return () => { - stdin?.off("data", onData); - }; - }, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]); - - // 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 - // accidental cancellations. - useInput( - (_input, key) => { - if (!key.escape) { - return; - } - - if (awaitingConfirm) { - log("useInput: second ESC detected – triggering onInterrupt()"); - onInterrupt(); - setAwaitingConfirm(false); - } else { - log("useInput: first ESC detected – waiting for confirmation"); - setAwaitingConfirm(true); - setTimeout(() => setAwaitingConfirm(false), 1500); - } - }, - { isActive: active }, - ); - - return ( - - - - {frameWithSeconds} - - Thinking - {dots} - - - - press Esc{" "} - {awaitingConfirm ? ( - again - ) : ( - twice - )}{" "} - to interrupt - - - - ); -} diff --git a/codex-cli/src/components/chat/terminal-chat-past-rollout.tsx b/codex-cli/src/components/chat/terminal-chat-past-rollout.tsx deleted file mode 100644 index 1ac8280edb..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-past-rollout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import type { TerminalChatSession } from "../../utils/session.js"; -import type { ResponseItem } from "openai/resources/responses/responses"; -import type { FileOpenerScheme } from "src/utils/config.js"; - -import TerminalChatResponseItem from "./terminal-chat-response-item"; -import { Box, Text } from "ink"; -import React from "react"; - -export default function TerminalChatPastRollout({ - session, - items, - fileOpener, -}: { - session: TerminalChatSession; - items: Array; - fileOpener: FileOpenerScheme | undefined; -}): React.ReactElement { - const { version, id: sessionId, model } = session; - return ( - - - - ● OpenAI Codex{" "} - - (research preview) v{version} - - - - - - localhost{" "} - · session:{" "} - - {sessionId} - - - - When / Who:{" "} - - {session.timestamp} / {session.user} - - - - model: {model} - - - - {React.useMemo( - () => - items.map((item, key) => ( - - )), - [items, fileOpener], - )} - - - ); -} diff --git a/codex-cli/src/components/chat/terminal-chat-response-item.tsx b/codex-cli/src/components/chat/terminal-chat-response-item.tsx deleted file mode 100644 index bab4aa317f..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-response-item.tsx +++ /dev/null @@ -1,360 +0,0 @@ -import type { OverlayModeType } from "./terminal-chat"; -import type { TerminalRendererOptions } from "marked-terminal"; -import type { - ResponseFunctionToolCallItem, - ResponseFunctionToolCallOutputItem, - ResponseInputMessageItem, - ResponseItem, - ResponseOutputMessage, - ResponseReasoningItem, -} from "openai/resources/responses/responses"; -import type { FileOpenerScheme } from "src/utils/config"; - -import { useTerminalSize } from "../../hooks/use-terminal-size"; -import { collapseXmlBlocks } from "../../utils/file-tag-utils"; -import { parseToolCall, parseToolCallOutput } from "../../utils/parsers"; -import chalk, { type ForegroundColorName } from "chalk"; -import { Box, Text } from "ink"; -import { parse, setOptions } from "marked"; -import TerminalRenderer from "marked-terminal"; -import path from "path"; -import React, { useEffect, useMemo } from "react"; -import { formatCommandForDisplay } from "src/format-command.js"; -import supportsHyperlinks from "supports-hyperlinks"; - -export default function TerminalChatResponseItem({ - item, - fullStdout = false, - setOverlayMode, - fileOpener, -}: { - item: ResponseItem; - fullStdout?: boolean; - setOverlayMode?: React.Dispatch>; - fileOpener: FileOpenerScheme | undefined; -}): React.ReactElement { - switch (item.type) { - case "message": - return ( - - ); - // @ts-expect-error new item types aren't in SDK yet - case "local_shell_call": - case "function_call": - return ; - // @ts-expect-error new item types aren't in SDK yet - case "local_shell_call_output": - case "function_call_output": - return ( - - ); - default: - break; - } - - // @ts-expect-error `reasoning` is not in the responses API yet - if (item.type === "reasoning") { - return ( - - ); - } - - return ; -} - -// TODO: this should be part of `ResponseReasoningItem`. Also it doesn't work. -// --------------------------------------------------------------------------- -// Utility helpers -// --------------------------------------------------------------------------- - -/** - * Guess how long the assistant spent "thinking" based on the combined length - * of the reasoning summary. The calculation itself is fast, but wrapping it in - * `useMemo` in the consuming component ensures it only runs when the - * `summary` array actually changes. - */ -// TODO: use actual thinking time -// -// function guessThinkingTime(summary: Array) { -// const totalTextLength = summary -// .map((t) => t.text.length) -// .reduce((a, b) => a + b, summary.length - 1); -// return Math.max(1, Math.ceil(totalTextLength / 300)); -// } - -export function TerminalChatResponseReasoning({ - message, - fileOpener, -}: { - message: ResponseReasoningItem & { duration_ms?: number }; - fileOpener: FileOpenerScheme | undefined; -}): React.ReactElement | null { - // Only render when there is a reasoning summary - if (!message.summary || message.summary.length === 0) { - return null; - } - return ( - - {message.summary.map((summary, key) => { - const s = summary as { headline?: string; text: string }; - return ( - - {s.headline && {s.headline}} - {s.text} - - ); - })} - - ); -} - -const colorsByRole: Record = { - assistant: "magentaBright", - user: "blueBright", -}; - -function TerminalChatResponseMessage({ - message, - setOverlayMode, - fileOpener, -}: { - message: ResponseInputMessageItem | ResponseOutputMessage; - setOverlayMode?: React.Dispatch>; - fileOpener: FileOpenerScheme | undefined; -}) { - // auto switch to model mode if the system message contains "has been deprecated" - useEffect(() => { - if (message.role === "system") { - const systemMessage = message.content.find( - (c) => c.type === "input_text", - )?.text; - if (systemMessage?.includes("model_not_found")) { - setOverlayMode?.("model"); - } - } - }, [message, setOverlayMode]); - - return ( - - - {message.role === "assistant" ? "codex" : message.role} - - - {message.content - .map( - (c) => - c.type === "output_text" - ? c.text - : c.type === "refusal" - ? c.refusal - : c.type === "input_text" - ? collapseXmlBlocks(c.text) - : c.type === "input_image" - ? "" - : c.type === "input_file" - ? c.filename - : "", // unknown content type - ) - .join(" ")} - - - ); -} - -function TerminalChatResponseToolCall({ - message, -}: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - message: ResponseFunctionToolCallItem | any; -}) { - let workdir: string | undefined; - let cmdReadableText: string | undefined; - if (message.type === "function_call") { - const details = parseToolCall(message); - workdir = details?.workdir; - cmdReadableText = details?.cmdReadableText; - } else if (message.type === "local_shell_call") { - const action = message.action; - workdir = action.working_directory; - cmdReadableText = formatCommandForDisplay(action.command); - } - return ( - - - command - {workdir ? {` (${workdir})`} : ""} - - - $ {cmdReadableText} - - - ); -} - -function TerminalChatResponseToolCallOutput({ - message, - fullStdout, -}: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - message: ResponseFunctionToolCallOutputItem | any; - fullStdout: boolean; -}) { - const { output, metadata } = parseToolCallOutput(message.output); - const { exit_code, duration_seconds } = metadata; - const metadataInfo = useMemo( - () => - [ - typeof exit_code !== "undefined" ? `code: ${exit_code}` : "", - typeof duration_seconds !== "undefined" - ? `duration: ${duration_seconds}s` - : "", - ] - .filter(Boolean) - .join(", "), - [exit_code, duration_seconds], - ); - let displayedContent = output; - if (message.type === "function_call_output" && !fullStdout) { - const lines = displayedContent.split("\n"); - if (lines.length > 4) { - const head = lines.slice(0, 4); - const remaining = lines.length - 4; - displayedContent = [...head, `... (${remaining} more lines)`].join("\n"); - } - } - - // ------------------------------------------------------------------------- - // Colorize diff output: lines starting with '-' in red, '+' in green. - // This makes patches and other diff‑like stdout easier to read. - // We exclude the typical diff file headers ('---', '+++') so they retain - // the default color. This is a best‑effort heuristic and should be safe for - // non‑diff output – only the very first character of a line is inspected. - // ------------------------------------------------------------------------- - const colorizedContent = displayedContent - .split("\n") - .map((line) => { - if (line.startsWith("+") && !line.startsWith("++")) { - return chalk.green(line); - } - if (line.startsWith("-") && !line.startsWith("--")) { - return chalk.red(line); - } - return line; - }) - .join("\n"); - return ( - - - command.stdout{" "} - {metadataInfo ? `(${metadataInfo})` : ""} - - {colorizedContent} - - ); -} - -export function TerminalChatResponseGenericMessage({ - message, -}: { - message: ResponseItem; -}): React.ReactElement { - return {JSON.stringify(message, null, 2)}; -} - -export type MarkdownProps = TerminalRendererOptions & { - children: string; - fileOpener: FileOpenerScheme | undefined; - /** Base path for resolving relative file citation paths. */ - cwd?: string; -}; - -export function Markdown({ - children, - fileOpener, - cwd, - ...options -}: MarkdownProps): React.ReactElement { - const size = useTerminalSize(); - - const rendered = React.useMemo(() => { - const linkifiedMarkdown = rewriteFileCitations(children, fileOpener, cwd); - - // Configure marked for this specific render - setOptions({ - // @ts-expect-error missing parser, space props - renderer: new TerminalRenderer({ ...options, width: size.columns }), - }); - const parsed = parse(linkifiedMarkdown, { async: false }).trim(); - - // Remove the truncation logic - return parsed; - // eslint-disable-next-line react-hooks/exhaustive-deps -- options is an object of primitives - }, [ - children, - size.columns, - size.rows, - fileOpener, - supportsHyperlinks.stdout, - chalk.level, - ]); - - return {rendered}; -} - -/** Regex to match citations for source files (hence the `F:` prefix). */ -const citationRegex = new RegExp( - [ - // Opening marker - "【", - - // Capture group 1: file ID or name (anything except '†') - "F:([^†]+)", - - // Field separator - "†", - - // Capture group 2: start line (digits) - "L(\\d+)", - - // Non-capturing group for optional end line - "(?:", - - // Capture group 3: end line (digits or '?') - "-L(\\d+|\\?)", - - // End of optional group (may not be present) - ")?", - - // Closing marker - "】", - ].join(""), - "g", // Global flag -); - -function rewriteFileCitations( - markdown: string, - fileOpener: FileOpenerScheme | undefined, - cwd: string = process.cwd(), -): string { - citationRegex.lastIndex = 0; - return markdown.replace(citationRegex, (_match, file, start, _end) => { - const absPath = path.resolve(cwd, file); - if (!fileOpener) { - return `[${file}](${absPath})`; - } - const uri = `${fileOpener}://file${absPath}:${start}`; - const label = `${file}:${start}`; - // In practice, sometimes multiple citations for the same file, but with a - // different line number, are shown sequentially, so we: - // - include the line number in the label to disambiguate them - // - add a space after the link to make it easier to read - return `[${label}](${uri}) `; - }); -} diff --git a/codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx b/codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx deleted file mode 100644 index 614ebf382f..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { parseApplyPatch } from "../../parse-apply-patch"; -import { shortenPath } from "../../utils/short-path"; -import chalk from "chalk"; -import { Text } from "ink"; -import React from "react"; - -export function TerminalChatToolCallCommand({ - commandForDisplay, - explanation, -}: { - commandForDisplay: string; - explanation?: string; -}): React.ReactElement { - // ------------------------------------------------------------------------- - // Colorize diff output inside the command preview: we detect individual - // lines that begin with '+' or '-' (excluding the typical diff headers like - // '+++', '---', '++', '--') and apply green/red coloring. This mirrors - // how Git shows diffs and makes the patch easier to review. - // ------------------------------------------------------------------------- - - const colorizedCommand = commandForDisplay - .split("\n") - .map((line) => { - if (line.startsWith("+") && !line.startsWith("++")) { - return chalk.green(line); - } - if (line.startsWith("-") && !line.startsWith("--")) { - return chalk.red(line); - } - return line; - }) - .join("\n"); - - return ( - <> - - Shell Command - - - $ {colorizedCommand} - - {explanation && ( - <> - - Explanation - - {explanation.split("\n").map((line, i) => { - // Apply different styling to headings (numbered items) - if (line.match(/^\d+\.\s+/)) { - return ( - - {line} - - ); - } else if (line.match(/^\s*\*\s+/)) { - // Style bullet points - return ( - - {line} - - ); - } else if (line.match(/^(WARNING|CAUTION|NOTE):/i)) { - // Style warnings - return ( - - {line} - - ); - } else { - return {line}; - } - })} - - )} - - ); -} - -export function TerminalChatToolCallApplyPatch({ - commandForDisplay, - patch, -}: { - commandForDisplay: string; - patch: string; -}): React.ReactElement { - const ops = React.useMemo(() => parseApplyPatch(patch), [patch]); - const firstOp = ops?.[0]; - - const title = React.useMemo(() => { - if (!firstOp) { - return ""; - } - return capitalize(firstOp.type); - }, [firstOp]); - - const filePath = React.useMemo(() => { - if (!firstOp) { - return ""; - } - return shortenPath(firstOp.path || "."); - }, [firstOp]); - - if (ops == null) { - return ( - <> - - Invalid Patch - - - The provided patch command is invalid. - - {commandForDisplay} - - ); - } - - if (!firstOp) { - return ( - <> - - Empty Patch - - - No operations found in the patch command. - - {commandForDisplay} - - ); - } - - return ( - <> - - {title} {filePath} - - - $ {commandForDisplay} - - - ); -} - -const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx deleted file mode 100644 index d41a94990f..0000000000 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ /dev/null @@ -1,766 +0,0 @@ -import type { AppRollout } from "../../app.js"; -import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; -import type { CommandConfirmation } from "../../utils/agent/agent-loop.js"; -import type { AppConfig } from "../../utils/config.js"; -import type { ColorName } from "chalk"; -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; - -import TerminalChatInput from "./terminal-chat-input.js"; -import TerminalChatPastRollout from "./terminal-chat-past-rollout.js"; -import { TerminalChatToolCallCommand } from "./terminal-chat-tool-call-command.js"; -import TerminalMessageHistory from "./terminal-message-history.js"; -import { formatCommandForDisplay } from "../../format-command.js"; -import { useConfirmation } from "../../hooks/use-confirmation.js"; -import { useTerminalSize } from "../../hooks/use-terminal-size.js"; -import { AgentLoop } from "../../utils/agent/agent-loop.js"; -import { ReviewDecision } from "../../utils/agent/review.js"; -import { generateCompactSummary } from "../../utils/compact-summary.js"; -import { saveConfig } 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 { log } from "../../utils/logger/log.js"; -import { - getAvailableModels, - calculateContextPercentRemaining, - uniqueById, -} from "../../utils/model-utils.js"; -import { createOpenAIClient } from "../../utils/openai-client.js"; -import { shortCwd } from "../../utils/short-path.js"; -import { saveRollout } from "../../utils/storage/save-rollout.js"; -import { CLI_VERSION } from "../../version.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 SessionsOverlay from "../sessions-overlay.js"; -import chalk from "chalk"; -import fs from "fs/promises"; -import { Box, Text } from "ink"; -import { spawn } from "node:child_process"; -import React, { useEffect, useMemo, useRef, useState } from "react"; -import { inspect } from "util"; - -export type OverlayModeType = - | "none" - | "history" - | "sessions" - | "model" - | "approval" - | "help" - | "diff"; - -type Props = { - config: AppConfig; - prompt?: string; - imagePaths?: Array; - approvalPolicy: ApprovalPolicy; - additionalWritableRoots: ReadonlyArray; - fullStdout: boolean; -}; - -const colorsByPolicy: Record = { - "suggest": undefined, - "auto-edit": "greenBright", - "full-auto": "green", -}; - -/** - * Generates an explanation for a shell command using the OpenAI API. - * - * @param command The command to explain - * @param model The model to use for generating the explanation - * @param flexMode Whether to use the flex-mode service tier - * @param config The configuration object - * @returns A human-readable explanation of what the command does - */ -async function generateCommandExplanation( - command: Array, - model: string, - flexMode: boolean, - config: AppConfig, -): Promise { - try { - // Create a temporary OpenAI client - const oai = createOpenAIClient(config); - - // Format the command for display - const commandForDisplay = formatCommandForDisplay(command); - - // 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", - content: - "You are an expert in shell commands and terminal operations. Your task is to provide detailed, accurate explanations of shell commands that users are considering executing. Break down each part of the command, explain what it does, identify any potential risks or side effects, and explain why someone might want to run it. Be specific about what files or systems will be affected. If the command could potentially be harmful, make sure to clearly highlight those risks.", - }, - { - role: "user", - content: `Please explain this shell command in detail: \`${commandForDisplay}\`\n\nProvide a structured explanation that includes:\n1. A brief overview of what the command does\n2. A breakdown of each part of the command (flags, arguments, etc.)\n3. What files, directories, or systems will be affected\n4. Any potential risks or side effects\n5. Why someone might want to run this command\n\nBe specific and technical - this explanation will help the user decide whether to approve or reject the command.`, - }, - ], - }); - - // Extract the explanation from the response - const explanation = - response.choices[0]?.message.content || "Unable to generate explanation."; - return explanation; - } catch (error) { - log(`Error generating command explanation: ${error}`); - - let errorMessage = "Unable to generate explanation due to an error."; - if (error instanceof Error) { - errorMessage = `Unable to generate explanation: ${error.message}`; - - // If it's an API error, check for more specific information - if ("status" in error && typeof error.status === "number") { - // Handle API-specific errors - if (error.status === 401) { - errorMessage = - "Unable to generate explanation: API key is invalid or expired."; - } else if (error.status === 429) { - errorMessage = - "Unable to generate explanation: Rate limit exceeded. Please try again later."; - } else if (error.status >= 500) { - errorMessage = - "Unable to generate explanation: OpenAI service is currently unavailable. Please try again later."; - } - } - } - - return errorMessage; - } -} - -export default function TerminalChat({ - config, - prompt: _initialPrompt, - imagePaths: _initialImagePaths, - approvalPolicy: initialApprovalPolicy, - additionalWritableRoots, - fullStdout, -}: Props): React.ReactElement { - const notify = Boolean(config.notify); - const [model, setModel] = useState(config.model); - const [provider, setProvider] = useState(config.provider || "openai"); - const [lastResponseId, setLastResponseId] = useState(null); - const [items, setItems] = useState>([]); - const [loading, setLoading] = useState(false); - const [approvalPolicy, setApprovalPolicy] = useState( - initialApprovalPolicy, - ); - const [thinkingSeconds, setThinkingSeconds] = useState(0); - - const handleCompact = async () => { - setLoading(true); - try { - const summary = await generateCompactSummary( - items, - model, - Boolean(config.flexMode), - config, - ); - setItems([ - { - id: `compact-${Date.now()}`, - type: "message", - role: "assistant", - content: [{ type: "output_text", text: summary }], - } as ResponseItem, - ]); - } catch (err) { - setItems((prev) => [ - ...prev, - { - id: `compact-error-${Date.now()}`, - type: "message", - role: "system", - content: [ - { type: "input_text", text: `Failed to compact context: ${err}` }, - ], - } as ResponseItem, - ]); - } finally { - setLoading(false); - } - }; - - const { - requestConfirmation, - confirmationPrompt, - explanation, - submitConfirmation, - } = useConfirmation(); - const [overlayMode, setOverlayMode] = useState("none"); - const [viewRollout, setViewRollout] = useState(null); - - // 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(""); - - const [initialPrompt, setInitialPrompt] = useState(_initialPrompt); - const [initialImagePaths, setInitialImagePaths] = - useState(_initialImagePaths); - - const PWD = React.useMemo(() => shortCwd(), []); - - // Keep a single AgentLoop instance alive across renders; - // recreate only when model/instructions/approvalPolicy change. - const agentRef = React.useRef(); - const [, forceUpdate] = React.useReducer((c) => c + 1, 0); // trigger re‑render - - // ──────────────────────────────────────────────────────────────── - // DEBUG: log every render w/ key bits of state - // ──────────────────────────────────────────────────────────────── - log( - `render - agent? ${Boolean(agentRef.current)} loading=${loading} items=${ - items.length - }`, - ); - - useEffect(() => { - // Skip recreating the agent if awaiting a decision on a pending confirmation. - if (confirmationPrompt != null) { - log("skip AgentLoop recreation due to pending confirmationPrompt"); - return; - } - - log("creating NEW AgentLoop"); - log( - `model=${model} provider=${provider} instructions=${Boolean( - config.instructions, - )} approvalPolicy=${approvalPolicy}`, - ); - - // Tear down any existing loop before creating a new one. - agentRef.current?.terminate(); - - const sessionId = crypto.randomUUID(); - agentRef.current = new AgentLoop({ - model, - provider, - config, - instructions: config.instructions, - approvalPolicy, - disableResponseStorage: config.disableResponseStorage, - additionalWritableRoots, - onLastResponseId: setLastResponseId, - onItem: (item) => { - log(`onItem: ${JSON.stringify(item)}`); - setItems((prev) => { - const updated = uniqueById([...prev, item as ResponseItem]); - saveRollout(sessionId, updated); - return updated; - }); - }, - onLoading: setLoading, - getCommandConfirmation: async ( - command: Array, - applyPatch: ApplyPatchCommand | undefined, - ): Promise => { - log(`getCommandConfirmation: ${command}`); - const commandForDisplay = formatCommandForDisplay(command); - - // First request for confirmation - let { decision: review, customDenyMessage } = await requestConfirmation( - , - ); - - // If the user wants an explanation, generate one and ask again. - if (review === ReviewDecision.EXPLAIN) { - log(`Generating explanation for command: ${commandForDisplay}`); - const explanation = await generateCommandExplanation( - command, - model, - Boolean(config.flexMode), - config, - ); - log(`Generated explanation: ${explanation}`); - - // Ask for confirmation again, but with the explanation. - const confirmResult = await requestConfirmation( - , - ); - - // Update the decision based on the second confirmation. - review = confirmResult.decision; - customDenyMessage = confirmResult.customDenyMessage; - - // Return the final decision with the explanation. - return { review, customDenyMessage, applyPatch, explanation }; - } - - return { review, customDenyMessage, applyPatch }; - }, - }); - - // Force a render so JSX below can "see" the freshly created agent. - forceUpdate(); - - log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); - - return () => { - log("terminating AgentLoop"); - agentRef.current?.terminate(); - agentRef.current = undefined; - forceUpdate(); // re‑render after teardown too - }; - // 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, provider, 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 - // re‑render every second during apply_patch reviews. - useEffect(() => { - let handle: ReturnType | null = null; - // Only tick the "thinking…" timer when the agent is actually processing - // a request *and* the user is not being asked to review a command. - if (loading && confirmationPrompt == null) { - setThinkingSeconds(0); - handle = setInterval(() => { - setThinkingSeconds((s) => s + 1); - }, 1000); - } else { - if (handle) { - clearInterval(handle); - } - setThinkingSeconds(0); - } - return () => { - if (handle) { - clearInterval(handle); - } - }; - }, [loading, confirmationPrompt]); - - // Notify desktop with a preview when an assistant response arrives. - const prevLoadingRef = useRef(false); - useEffect(() => { - // Only notify when notifications are enabled. - if (!notify) { - prevLoadingRef.current = loading; - return; - } - - if ( - prevLoadingRef.current && - !loading && - confirmationPrompt == null && - items.length > 0 - ) { - if (process.platform === "darwin") { - // find the last assistant message - const assistantMessages = items.filter( - (i) => i.type === "message" && i.role === "assistant", - ); - const last = assistantMessages[assistantMessages.length - 1]; - if (last) { - const text = last.content - .map((c) => { - if (c.type === "output_text") { - return c.text; - } - return ""; - }) - .join("") - .trim(); - const preview = text.replace(/\n/g, " ").slice(0, 100); - const safePreview = preview.replace(/"/g, '\\"'); - const title = "Codex CLI"; - const cwd = PWD; - spawn("osascript", [ - "-e", - `display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"`, - ]); - } - } - } - prevLoadingRef.current = loading; - }, [notify, loading, confirmationPrompt, items, PWD]); - - // Let's also track whenever the ref becomes available. - const agent = agentRef.current; - useEffect(() => { - log(`agentRef.current is now ${Boolean(agent)}`); - }, [agent]); - - // --------------------------------------------------------------------- - // Dynamic layout constraints – keep total rendered rows <= terminal rows - // --------------------------------------------------------------------- - - const { rows: terminalRows } = useTerminalSize(); - - useEffect(() => { - const processInitialInputItems = async () => { - if ( - (!initialPrompt || initialPrompt.trim() === "") && - (!initialImagePaths || initialImagePaths.length === 0) - ) { - return; - } - const inputItems = [ - await createInputItem(initialPrompt || "", initialImagePaths || []), - ]; - // Clear them to prevent subsequent runs. - setInitialPrompt(""); - setInitialImagePaths([]); - agent?.run(inputItems); - }; - processInitialInputItems(); - }, [agent, initialPrompt, initialImagePaths]); - - // ──────────────────────────────────────────────────────────────── - // In-app warning if CLI --model isn't in fetched list - // ──────────────────────────────────────────────────────────────── - useEffect(() => { - (async () => { - const available = await getAvailableModels(provider); - if (model && available.length > 0 && !available.includes(model)) { - setItems((prev) => [ - ...prev, - { - id: `unknown-model-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Warning: model "${model}" is not in the list of available models for provider "${provider}".`, - }, - ], - }, - ]); - } - })(); - // run once on mount - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Just render every item in order, no grouping/collapse. - const lastMessageBatch = items.map((item) => ({ item })); - const groupCounts: Record = {}; - const userMsgCount = items.filter( - (i) => i.type === "message" && i.role === "user", - ).length; - - const contextLeftPercent = useMemo( - () => calculateContextPercentRemaining(items, model), - [items, model], - ); - - if (viewRollout) { - return ( - - ); - } - - return ( - - - {agent ? ( - - ) : ( - - Initializing agent… - - )} - {overlayMode === "none" && agent && ( - - submitConfirmation({ - decision, - customDenyMessage, - }) - } - contextLeftPercent={contextLeftPercent} - openOverlay={() => setOverlayMode("history")} - openModelOverlay={() => setOverlayMode("model")} - openApprovalOverlay={() => setOverlayMode("approval")} - openHelpOverlay={() => setOverlayMode("help")} - openSessionsOverlay={() => setOverlayMode("sessions")} - 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={() => { - if (!agent) { - return; - } - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - agent.cancel(); - setLoading(false); - - // Add a system message to indicate the interruption - setItems((prev) => [ - ...prev, - { - id: `interrupt-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: "⏹️ Execution interrupted by user. You can continue typing.", - }, - ], - }, - ]); - }} - submitInput={(inputs) => { - agent.run(inputs, lastResponseId || ""); - return {}; - }} - items={items} - thinkingSeconds={thinkingSeconds} - /> - )} - {overlayMode === "history" && ( - setOverlayMode("none")} /> - )} - {overlayMode === "sessions" && ( - { - try { - const txt = await fs.readFile(p, "utf-8"); - const data = JSON.parse(txt) as AppRollout; - setViewRollout(data); - setOverlayMode("none"); - } catch { - setOverlayMode("none"); - } - }} - onResume={(p) => { - setOverlayMode("none"); - setInitialPrompt(`Resume this session: ${p}`); - }} - onExit={() => setOverlayMode("none")} - /> - )} - {overlayMode === "model" && ( - { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - if (!agent) { - log("TerminalChat: agent is not ready yet"); - } - agent?.cancel(); - setLoading(false); - - if (!allModels?.includes(newModel)) { - // eslint-disable-next-line no-console - console.error( - chalk.bold.red( - `Model "${chalk.yellow( - newModel, - )}" is not available for provider "${chalk.yellow( - provider, - )}".`, - ), - ); - return; - } - - setModel(newModel); - setLastResponseId((prev) => - prev && newModel !== model ? null : prev, - ); - - // Save model to config - saveConfig({ - ...config, - model: newModel, - provider: provider, - }); - - setItems((prev) => [ - ...prev, - { - id: `switch-model-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Switched model to ${newModel}`, - }, - ], - }, - ]); - - setOverlayMode("none"); - }} - onSelectProvider={(newProvider) => { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - if (!agent) { - log("TerminalChat: agent is not ready yet"); - } - agent?.cancel(); - setLoading(false); - - // Select default model for the new provider. - const defaultModel = model; - - // Save provider to config. - const updatedConfig = { - ...config, - provider: newProvider, - model: defaultModel, - }; - saveConfig(updatedConfig); - - setProvider(newProvider); - setModel(defaultModel); - setLastResponseId((prev) => - prev && newProvider !== provider ? null : prev, - ); - - setItems((prev) => [ - ...prev, - { - id: `switch-provider-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Switched provider to ${newProvider} with model ${defaultModel}`, - }, - ], - }, - ]); - - // Don't close the overlay so user can select a model for the new provider - // setOverlayMode("none"); - }} - onExit={() => setOverlayMode("none")} - /> - )} - - {overlayMode === "approval" && ( - { - // Update approval policy without cancelling an in-progress session. - if (newMode === approvalPolicy) { - return; - } - - setApprovalPolicy(newMode as ApprovalPolicy); - if (agentRef.current) { - ( - agentRef.current as unknown as { - approvalPolicy: ApprovalPolicy; - } - ).approvalPolicy = newMode as ApprovalPolicy; - } - setItems((prev) => [ - ...prev, - { - id: `switch-approval-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Switched approval mode to ${newMode}`, - }, - ], - }, - ]); - - setOverlayMode("none"); - }} - onExit={() => setOverlayMode("none")} - /> - )} - - {overlayMode === "help" && ( - setOverlayMode("none")} /> - )} - - {overlayMode === "diff" && ( - setOverlayMode("none")} - /> - )} - - - ); -} diff --git a/codex-cli/src/components/chat/terminal-header.tsx b/codex-cli/src/components/chat/terminal-header.tsx deleted file mode 100644 index 9ba16e6fde..0000000000 --- a/codex-cli/src/components/chat/terminal-header.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import type { AgentLoop } from "../../utils/agent/agent-loop.js"; - -import { Box, Text } from "ink"; -import path from "node:path"; -import React from "react"; - -export interface TerminalHeaderProps { - terminalRows: number; - version: string; - PWD: string; - model: string; - provider?: string; - approvalPolicy: string; - colorsByPolicy: Record; - agent?: AgentLoop; - initialImagePaths?: Array; - flexModeEnabled?: boolean; -} - -const TerminalHeader: React.FC = ({ - terminalRows, - version, - PWD, - model, - provider = "openai", - approvalPolicy, - colorsByPolicy, - agent, - initialImagePaths, - flexModeEnabled = false, -}) => { - return ( - <> - {terminalRows < 10 ? ( - // Compact header for small terminal windows - - ● Codex v{version} - {PWD} - {model} ({provider}) -{" "} - {approvalPolicy} - {flexModeEnabled ? " - flex-mode" : ""} - - ) : ( - <> - - - ● OpenAI Codex{" "} - - (research preview) v{version} - - - - - - localhost session:{" "} - - {agent?.sessionId ?? ""} - - - - workdir: {PWD} - - - model: {model} - - - provider:{" "} - {provider} - - - approval:{" "} - - {approvalPolicy} - - - {flexModeEnabled && ( - - flex-mode:{" "} - enabled - - )} - {initialImagePaths?.map((img, idx) => ( - - image:{" "} - {path.basename(img)} - - ))} - - - )} - - ); -}; - -export default TerminalHeader; diff --git a/codex-cli/src/components/chat/terminal-message-history.tsx b/codex-cli/src/components/chat/terminal-message-history.tsx deleted file mode 100644 index 5036f0813d..0000000000 --- a/codex-cli/src/components/chat/terminal-message-history.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import type { OverlayModeType } from "./terminal-chat.js"; -import type { TerminalHeaderProps } from "./terminal-header.js"; -import type { GroupedResponseItem } from "./use-message-grouping.js"; -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; -import type { FileOpenerScheme } from "src/utils/config.js"; - -import TerminalChatResponseItem from "./terminal-chat-response-item.js"; -import TerminalHeader from "./terminal-header.js"; -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 -// items (e.g. auto‑approved tool‑call batches) that should be rendered -// together. -type BatchEntry = { item?: ResponseItem; group?: GroupedResponseItem }; -type TerminalMessageHistoryProps = { - batch: Array; - groupCounts: Record; - items: Array; - userMsgCount: number; - confirmationPrompt: React.ReactNode; - loading: boolean; - thinkingSeconds: number; - headerProps: TerminalHeaderProps; - fullStdout: boolean; - setOverlayMode: React.Dispatch>; - fileOpener: FileOpenerScheme | undefined; -}; - -const TerminalMessageHistory: React.FC = ({ - batch, - headerProps, - // `loading` and `thinkingSeconds` handled by input component now. - loading: _loading, - thinkingSeconds: _thinkingSeconds, - fullStdout, - setOverlayMode, - fileOpener, -}) => { - // Flatten batch entries to response items. - const messages = useMemo(() => batch.map(({ item }) => item!), [batch]); - - return ( - - {/* The dedicated thinking indicator in the input area now displays the - elapsed time, so we no longer render a separate counter here. */} - - {(item, index) => { - if (item === "header") { - return ; - } - - // After the guard above, item is a ResponseItem - const message = item as ResponseItem; - // Suppress empty reasoning updates (i.e. items with an empty summary). - const msg = message as unknown as { summary?: Array }; - if (msg.summary?.length === 0) { - return null; - } - return ( - - - - ); - }} - - - ); -}; - -export default React.memo(TerminalMessageHistory); diff --git a/codex-cli/src/components/chat/use-message-grouping.ts b/codex-cli/src/components/chat/use-message-grouping.ts deleted file mode 100644 index 1e526821d0..0000000000 --- a/codex-cli/src/components/chat/use-message-grouping.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; - -/** - * Represents a grouped sequence of response items (e.g., function call batches). - */ -export type GroupedResponseItem = { - label: string; - items: Array; -}; diff --git a/codex-cli/src/components/diff-overlay.tsx b/codex-cli/src/components/diff-overlay.tsx deleted file mode 100644 index 8de85b87d5..0000000000 --- a/codex-cli/src/components/diff-overlay.tsx +++ /dev/null @@ -1,93 +0,0 @@ -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 ( - - {line === "" ? " " : line} - - ); - } - - return ( - - - Working tree diff ({lines.length} lines) - - - - {visible.map((line, idx) => { - return renderLine(line, firstVisible + idx); - })} - - - - esc Close ↑↓ Scroll PgUp/PgDn g/G First/Last - - - ); -} diff --git a/codex-cli/src/components/help-overlay.tsx b/codex-cli/src/components/help-overlay.tsx deleted file mode 100644 index 1c24ad9c72..0000000000 --- a/codex-cli/src/components/help-overlay.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Box, Text, useInput } from "ink"; -import React from "react"; - -/** - * An overlay that lists the available slash‑commands and their description. - * The overlay is purely informational and can be dismissed with the Escape - * key. Keeping the implementation extremely small avoids adding any new - * dependencies or complex state handling. - */ -export default function HelpOverlay({ - onExit, -}: { - onExit: () => void; -}): JSX.Element { - useInput((input, key) => { - if (key.escape || input === "q") { - onExit(); - } - }); - - return ( - - - Available commands - - - - - Slash‑commands - - - /help – show this help overlay - - - /model – switch the LLM model in‑session - - - /approval – switch auto‑approval mode - - - /history – show command & file history - for this session - - - /clear – clear screen & context - - - /clearhistory – clear command history - - - /bug – generate a prefilled GitHub issue URL - with session log - - - /diff – view working tree git diff - - - /compact – condense context into a summary - - - - - Keyboard shortcuts - - - - Enter – send message - - - Ctrl+J – insert newline - - {/* Re-enable once we re-enable new input */} - {/* - - Ctrl+X/Ctrl+E -  – open external editor ($EDITOR) - - */} - - Up/Down – scroll prompt history - - - - Esc(✕2) - {" "} - – interrupt current action - - - Ctrl+C – quit Codex - - - - - Esc or q to close - - - ); -} diff --git a/codex-cli/src/components/history-overlay.tsx b/codex-cli/src/components/history-overlay.tsx deleted file mode 100644 index f6ea8464e1..0000000000 --- a/codex-cli/src/components/history-overlay.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; - -import { Box, Text, useInput } from "ink"; -import React, { useMemo, useState } from "react"; - -type Props = { - items: Array; - onExit: () => void; -}; - -type Mode = "commands" | "files"; - -export default function HistoryOverlay({ items, onExit }: Props): JSX.Element { - const [mode, setMode] = useState("commands"); - const [cursor, setCursor] = useState(0); - - const { commands, files } = useMemo( - () => formatHistoryForDisplay(items), - [items], - ); - - const list = mode === "commands" ? commands : files; - - useInput((input, key) => { - if (key.escape) { - onExit(); - return; - } - - if (input === "c") { - setMode("commands"); - setCursor(0); - return; - } - if (input === "f") { - setMode("files"); - setCursor(0); - return; - } - - if (key.downArrow || input === "j") { - setCursor((c) => Math.min(list.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(list.length - 1, c + 10)); - } else if (key.pageUp) { - setCursor((c) => Math.max(0, c - 10)); - } else if (input === "g") { - setCursor(0); - } else if (input === "G") { - setCursor(list.length - 1); - } - }); - - const rows = process.stdout.rows || 24; - const headerRows = 2; - const footerRows = 1; - const maxVisible = Math.max(4, rows - headerRows - footerRows); - - const firstVisible = Math.min( - Math.max(0, cursor - Math.floor(maxVisible / 2)), - Math.max(0, list.length - maxVisible), - ); - const visible = list.slice(firstVisible, firstVisible + maxVisible); - - return ( - - - - {mode === "commands" ? "Commands run" : "Files touched"} ( - {list.length}) - - - - {visible.map((txt, idx) => { - const absIdx = firstVisible + idx; - const selected = absIdx === cursor; - return ( - - {selected ? "› " : " "} - {txt} - - ); - })} - - - - esc Close ↑↓ Scroll PgUp/PgDn g/G First/Last c Commands f Files - - - - ); -} - -function formatHistoryForDisplay(items: Array): { - commands: Array; - files: Array; -} { - const commands: Array = []; - const filesSet = new Set(); - - for (const item of items) { - const userPrompt = processUserMessage(item); - if (userPrompt) { - commands.push(userPrompt); - continue; - } - - // ------------------------------------------------------------------ - // We are interested in tool calls which – for the OpenAI client – are - // represented as `function_call` response items. Skip everything else. - if (item.type !== "function_call") { - continue; - } - - const { name: toolName, arguments: argsString } = item as unknown as { - name: unknown; - arguments: unknown; - }; - - if (typeof argsString !== "string") { - // Malformed – still record the tool name to give users maximal context. - if (typeof toolName === "string" && toolName.length > 0) { - commands.push(toolName); - } - continue; - } - - // Best‑effort attempt to parse the JSON arguments. We never throw on parse - // failure – the history view must be resilient to bad data. - let argsJson: unknown = undefined; - try { - argsJson = JSON.parse(argsString); - } catch { - argsJson = undefined; - } - - // 1) Shell / exec‑like tool calls expose a `cmd` or `command` property - // that is an array of strings. These are rendered as the joined command - // line for familiarity with traditional shells. - const argsObj = argsJson as Record | undefined; - const cmdArray: Array | undefined = Array.isArray(argsObj?.["cmd"]) - ? (argsObj!["cmd"] as Array) - : Array.isArray(argsObj?.["command"]) - ? (argsObj!["command"] as Array) - : undefined; - - if (cmdArray && cmdArray.length > 0) { - commands.push(processCommandArray(cmdArray, filesSet)); - continue; // We processed this as a command; no need to treat as generic tool call. - } - - // 2) Non‑exec tool calls – we fall back to recording the tool name plus a - // short argument representation to give users an idea of what - // happened. - if (typeof toolName === "string" && toolName.length > 0) { - commands.push(processNonExecTool(toolName, argsJson, filesSet)); - } - } - - return { commands, files: Array.from(filesSet) }; -} - -function processUserMessage(item: ResponseItem): string | null { - if ( - item.type === "message" && - (item as unknown as { role?: string }).role === "user" - ) { - // TODO: We're ignoring images/files here. - const parts = - (item as unknown as { content?: Array }).content ?? []; - const texts: Array = []; - if (Array.isArray(parts)) { - for (const part of parts) { - if (part && typeof part === "object" && "text" in part) { - const t = (part as unknown as { text?: string }).text; - if (typeof t === "string" && t.length > 0) { - texts.push(t); - } - } - } - } - - if (texts.length > 0) { - const fullPrompt = texts.join(" "); - // Truncate very long prompts so the history view stays legible. - return fullPrompt.length > 120 - ? `> ${fullPrompt.slice(0, 117)}…` - : `> ${fullPrompt}`; - } - } - return null; -} - -function processCommandArray( - cmdArray: Array, - filesSet: Set, -): string { - const cmd = cmdArray.join(" "); - - // Heuristic for file paths in command args - for (const part of cmdArray) { - if (!part.startsWith("-") && part.includes("/")) { - filesSet.add(part); - } - } - - // Special‑case apply_patch so we can extract the list of modified files - if (cmdArray[0] === "apply_patch" || cmdArray.includes("apply_patch")) { - const patchTextMaybe = cmdArray.find((s) => s.includes("*** Begin Patch")); - if (typeof patchTextMaybe === "string") { - const lines = patchTextMaybe.split("\n"); - for (const line of lines) { - const m = line.match(/^[-+]{3} [ab]\/(.+)$/); - if (m && m[1]) { - filesSet.add(m[1]); - } - } - } - } - - return cmd; -} - -function processNonExecTool( - toolName: string, - argsJson: unknown, - filesSet: Set, -): string { - let summary = toolName; - - if (argsJson && typeof argsJson === "object") { - // Extract a few common argument keys to make the summary more useful - // without being overly verbose. - const interestingKeys = ["path", "file", "filepath", "filename", "pattern"]; - for (const key of interestingKeys) { - const val = (argsJson as Record)[key]; - if (typeof val === "string") { - summary += ` ${val}`; - if (val.includes("/")) { - filesSet.add(val); - } - break; - } - } - } - - return summary; -} diff --git a/codex-cli/src/components/model-overlay.tsx b/codex-cli/src/components/model-overlay.tsx deleted file mode 100644 index 86a7e5850d..0000000000 --- a/codex-cli/src/components/model-overlay.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import TypeaheadOverlay from "./typeahead-overlay.js"; -import { - getAvailableModels, - RECOMMENDED_MODELS as _RECOMMENDED_MODELS, -} from "../utils/model-utils.js"; -import { Box, Text, useInput } from "ink"; -import React, { useEffect, useState } from "react"; - -/** - * Props for . - * - * When `hasLastResponse` is true the user has already received at least one - * assistant response in the current session which means switching models is no - * longer supported – the overlay should therefore show an error and only allow - * the user to close it. - */ -type Props = { - currentModel: string; - currentProvider?: string; - hasLastResponse: boolean; - providers?: Record; - onSelect: (allModels: Array, model: string) => void; - onSelectProvider?: (provider: string) => void; - onExit: () => void; -}; - -export default function ModelOverlay({ - currentModel, - providers = {}, - currentProvider = "openai", - hasLastResponse, - onSelect, - onSelectProvider, - onExit, -}: Props): JSX.Element { - const [items, setItems] = useState>( - [], - ); - const [providerItems, _setProviderItems] = useState< - Array<{ label: string; value: string }> - >(Object.values(providers).map((p) => ({ label: p.name, value: p.name }))); - const [mode, setMode] = useState<"model" | "provider">("model"); - const [isLoading, setIsLoading] = useState(true); - - // This effect will run when the provider changes to update the model list - useEffect(() => { - setIsLoading(true); - (async () => { - try { - const models = await getAvailableModels(currentProvider); - // Convert the models to the format needed by TypeaheadOverlay - setItems( - models.map((m) => ({ - label: m, - value: m, - })), - ); - } catch (error) { - // Silently handle errors - remove console.error - // console.error("Error loading models:", error); - } finally { - setIsLoading(false); - } - })(); - }, [currentProvider]); - - // --------------------------------------------------------------------------- - // If the conversation already contains a response we cannot change the model - // anymore because the backend requires a consistent model across the entire - // run. In that scenario we replace the regular typeahead picker with a - // simple message instructing the user to start a new chat. The only - // available action is to dismiss the overlay (Esc or Enter). - // --------------------------------------------------------------------------- - - // Register input handling for switching between model and provider selection - useInput((_input, key) => { - if (hasLastResponse && (key.escape || key.return)) { - onExit(); - } else if (!hasLastResponse) { - if (key.tab) { - setMode(mode === "model" ? "provider" : "model"); - } - } - }); - - if (hasLastResponse) { - return ( - - - - Unable to switch model - - - - - You can only pick a model before the assistant sends its first - response. To use a different model please start a new chat. - - - - press esc or enter to close - - - ); - } - - if (mode === "provider") { - return ( - - - Current provider:{" "} - {currentProvider} - - press tab to switch to model selection - - } - initialItems={providerItems} - currentValue={currentProvider} - onSelect={(provider) => { - if (onSelectProvider) { - onSelectProvider(provider); - // Immediately switch to model selection so user can pick a model for the new provider - setMode("model"); - } - }} - onExit={onExit} - /> - ); - } - - return ( - - - Current model: {currentModel} - - - Current provider: {currentProvider} - - {isLoading && Loading models...} - press tab to switch to provider selection - - } - initialItems={items} - currentValue={currentModel} - onSelect={(selectedModel) => - onSelect( - items?.map((m) => m.value), - selectedModel, - ) - } - onExit={onExit} - /> - ); -} diff --git a/codex-cli/src/components/onboarding/onboarding-approval-mode.tsx b/codex-cli/src/components/onboarding/onboarding-approval-mode.tsx deleted file mode 100644 index f095c6c04b..0000000000 --- a/codex-cli/src/components/onboarding/onboarding-approval-mode.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-expect-error select.js is JavaScript and has no types -import { Select } from "../vendor/ink-select/select"; -import { Box, Text } from "ink"; -import React from "react"; -import { AutoApprovalMode } from "src/utils/auto-approval-mode"; - -// TODO: figure out why `cli-spinners` fails on Node v20.9.0 -// which is why we have to do this in the first place - -export function OnboardingApprovalMode(): React.ReactElement { - return ( - - Choose what you want to have to approve: -