mirror of
https://github.com/openai/codex.git
synced 2026-02-02 15:03:38 +00:00
Compare commits
251 Commits
remove/doc
...
agentydrag
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
697746788d | ||
|
|
f51d888c73 | ||
|
|
c75de9a451 | ||
|
|
c70bd0aef8 | ||
|
|
3374a8bc3a | ||
|
|
601ba4ed04 | ||
|
|
9695fbb497 | ||
|
|
b2fa563036 | ||
|
|
b0a669ebc9 | ||
|
|
67c9f755fa | ||
|
|
8854fbdb06 | ||
|
|
4e7840454b | ||
|
|
2e2531e466 | ||
|
|
253324e5c3 | ||
|
|
4179fbf51b | ||
|
|
4f827c6ac2 | ||
|
|
4ed4b72e26 | ||
|
|
d4aaa1aad4 | ||
|
|
fa2a5d07bf | ||
|
|
da247e932c | ||
|
|
d967dbc669 | ||
|
|
244d2389ac | ||
|
|
e18744b9ec | ||
|
|
d6dd46c2e6 | ||
|
|
a30a80d22a | ||
|
|
94167c15cf | ||
|
|
3d9e034c65 | ||
|
|
c9a37141ba | ||
|
|
9638d55e08 | ||
|
|
96ac1bcbed | ||
|
|
5260f2360c | ||
|
|
1d86ea366d | ||
|
|
98dc2c8482 | ||
|
|
6521b84369 | ||
|
|
f106686146 | ||
|
|
079ee5f6e3 | ||
|
|
3d3bc8f765 | ||
|
|
6f1d48b489 | ||
|
|
d2ca0b9265 | ||
|
|
c4b1beea57 | ||
|
|
ca4cf88334 | ||
|
|
6749b3c1ea | ||
|
|
b841b834fe | ||
|
|
bf0e850325 | ||
|
|
226871139b | ||
|
|
44bf98533b | ||
|
|
6a660f616e | ||
|
|
9bf7f28b31 | ||
|
|
86539bd522 | ||
|
|
d245d74d56 | ||
|
|
a349b2e0ba | ||
|
|
2ab6be6cdb | ||
|
|
88d46eb754 | ||
|
|
a1598d645c | ||
|
|
53efec12b1 | ||
|
|
a4a2680b39 | ||
|
|
3683bae697 | ||
|
|
2d04f7332a | ||
|
|
4acf48dd56 | ||
|
|
0f6dde9621 | ||
|
|
2c1c65a004 | ||
|
|
6059f47a79 | ||
|
|
4cc4236fe6 | ||
|
|
1bff7c4db0 | ||
|
|
e6f8f37104 | ||
|
|
05a49d8036 | ||
|
|
905e85cdf0 | ||
|
|
da1df276a2 | ||
|
|
de380981de | ||
|
|
27b3596809 | ||
|
|
442bc9b9c3 | ||
|
|
6370f49f9d | ||
|
|
23cb831893 | ||
|
|
15b895e362 | ||
|
|
8aaafa0f43 | ||
|
|
c9613d41e5 | ||
|
|
9b2cb58ce5 | ||
|
|
87e79cecdf | ||
|
|
478c04bf14 | ||
|
|
e83f5e8e6c | ||
|
|
f64427aea2 | ||
|
|
1d35b96d86 | ||
|
|
cc65aa0882 | ||
|
|
ed15fc9e7d | ||
|
|
0a9c0304f8 | ||
|
|
9f30c4df50 | ||
|
|
8280bd1f9d | ||
|
|
928afbb87e | ||
|
|
7f7582d68d | ||
|
|
557d7e42db | ||
|
|
2f254365e0 | ||
|
|
21ee410932 | ||
|
|
1f6385392d | ||
|
|
40f784e5c3 | ||
|
|
b352146912 | ||
|
|
211c1ae28f | ||
|
|
2ce7ba7257 | ||
|
|
a73f150bf1 | ||
|
|
734ba5ae6a | ||
|
|
44323ac115 | ||
|
|
bbce68d8c3 | ||
|
|
dc7e8e9c5b | ||
|
|
5f70dc1d21 | ||
|
|
14b1c9f909 | ||
|
|
b54f7b6a6e | ||
|
|
fedcedf983 | ||
|
|
158d8a21f8 | ||
|
|
a07b897c34 | ||
|
|
332b6666b8 | ||
|
|
801eeb5841 | ||
|
|
41b7330e86 | ||
|
|
6266f49fc8 | ||
|
|
1f1c30f87e | ||
|
|
7f7a5420fb | ||
|
|
fd0dc1be88 | ||
|
|
172bd4f870 | ||
|
|
e901754d14 | ||
|
|
b197387b7d | ||
|
|
e7f7847a49 | ||
|
|
c091d4ac64 | ||
|
|
68f11484f7 | ||
|
|
899a958168 | ||
|
|
d8c6ee32e6 | ||
|
|
fca6766935 | ||
|
|
9faf44626d | ||
|
|
624deb4158 | ||
|
|
043ca32785 | ||
|
|
59bf45c30d | ||
|
|
2a015a2464 | ||
|
|
1fb774909d | ||
|
|
d07d7a7440 | ||
|
|
771294f65d | ||
|
|
b1eb965839 | ||
|
|
90fa512e8b | ||
|
|
723c2452e0 | ||
|
|
27ad99f4be | ||
|
|
26296b9872 | ||
|
|
47d967d44d | ||
|
|
cba2c91fce | ||
|
|
acd128f38d | ||
|
|
74bc491c94 | ||
|
|
3d68ba3e06 | ||
|
|
9670cfdef8 | ||
|
|
fc249fcf8f | ||
|
|
b618d3a8ea | ||
|
|
8a6a6dc3fb | ||
|
|
3fd329294d | ||
|
|
18177f98ae | ||
|
|
2e284fbf3b | ||
|
|
e7f49ec30c | ||
|
|
3eb6efd4a4 | ||
|
|
71806ef029 | ||
|
|
5d709841e3 | ||
|
|
f26875b5d8 | ||
|
|
f2bd29127c | ||
|
|
cdf3b5ac5a | ||
|
|
d5668f158d | ||
|
|
30989812e6 | ||
|
|
2e38a2095c | ||
|
|
301a3631db | ||
|
|
9969ab9f86 | ||
|
|
444df1f1a0 | ||
|
|
7d9eef2034 | ||
|
|
d78cfb6330 | ||
|
|
0e2afc0378 | ||
|
|
268bf1d189 | ||
|
|
23f0f65cdb | ||
|
|
96f28a5896 | ||
|
|
2688483b9a | ||
|
|
fcb6828574 | ||
|
|
fb90672a81 | ||
|
|
ed848595de | ||
|
|
d7d15f5fd3 | ||
|
|
8ff34a0108 | ||
|
|
4851101038 | ||
|
|
81c8bbe600 | ||
|
|
9dc939721c | ||
|
|
1284704e13 | ||
|
|
50e69ec7a3 | ||
|
|
98e963beac | ||
|
|
8a98ea587b | ||
|
|
62705cbe48 | ||
|
|
1d16069948 | ||
|
|
940351d8b1 | ||
|
|
0fb9c2440b | ||
|
|
7a29431ad1 | ||
|
|
0456efdec7 | ||
|
|
2620fff775 | ||
|
|
4d8df74cec | ||
|
|
648301613c | ||
|
|
35f89f44d2 | ||
|
|
056f818002 | ||
|
|
e73d97acf1 | ||
|
|
2379dcd0ed | ||
|
|
11fabccebb | ||
|
|
09e4a3b4b8 | ||
|
|
669aa3ef39 | ||
|
|
601c61f2cb | ||
|
|
4c10e86c59 | ||
|
|
f76792cfc3 | ||
|
|
95dbfa37b5 | ||
|
|
a371729a2b | ||
|
|
83d4176fcf | ||
|
|
4989965f1b | ||
|
|
88966daee9 | ||
|
|
44ea1cf73f | ||
|
|
c5f4fafb54 | ||
|
|
5f6352bbfa | ||
|
|
fb4857acb1 | ||
|
|
22d7b327a0 | ||
|
|
88e53a2ae9 | ||
|
|
df9e65127f | ||
|
|
b23d44cb5c | ||
|
|
09d975e93b | ||
|
|
b0f14f1f55 | ||
|
|
2f0109faeb | ||
|
|
a86790c553 | ||
|
|
cb0051f274 | ||
|
|
58975e5db1 | ||
|
|
ad5e28d96a | ||
|
|
83ce407baf | ||
|
|
c370c0cca6 | ||
|
|
aa0a4bfba4 | ||
|
|
12dced2d53 | ||
|
|
3fe8b3c5a9 | ||
|
|
84a4e64bf1 | ||
|
|
5c62d7d42c | ||
|
|
1df8e3c308 | ||
|
|
2e3e115128 | ||
|
|
3762c71818 | ||
|
|
4eedb33a1f | ||
|
|
a527582e32 | ||
|
|
1c2722335d | ||
|
|
62dda0b041 | ||
|
|
771017c5be | ||
|
|
872e89f8e8 | ||
|
|
a36ab4797c | ||
|
|
481a76458f | ||
|
|
db8e6f3255 | ||
|
|
489d2db021 | ||
|
|
52c7328242 | ||
|
|
a6050c920b | ||
|
|
8d13b6a652 | ||
|
|
f84ed7d584 | ||
|
|
32c9c0d23b | ||
|
|
f2e27c46d6 | ||
|
|
719eb2a0f0 | ||
|
|
65e5d926ab | ||
|
|
bb19ef1434 | ||
|
|
c37d82e3d5 | ||
|
|
e585e6c8cd |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -48,6 +48,12 @@ yarn-error.log*
|
||||
|
||||
# env
|
||||
.env*
|
||||
|
||||
# oaipkg import cache
|
||||
oaipkg/
|
||||
|
||||
# Ignore task worktree directories created by create-task-worktree.sh
|
||||
agentydragon/tasks/.worktrees/
|
||||
!.env.example
|
||||
|
||||
# package
|
||||
@@ -81,3 +87,7 @@ CHANGELOG.ignore.md
|
||||
# nix related
|
||||
.direnv
|
||||
.envrc
|
||||
|
||||
__pycache__
|
||||
|
||||
codex-rs/target
|
||||
|
||||
15
.pre-commit-config.yaml
Normal file
15
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: check-tasks
|
||||
name: Run all task-directory validation checks
|
||||
entry: python3 agentydragon/tools/check_tasks.py
|
||||
language: python
|
||||
additional_dependencies: [PyYAML, toml, pydantic]
|
||||
files: ^agentydragon/tasks/.*
|
||||
- id: cargo-build
|
||||
name: Check Rust workspace and linux-sandbox compile
|
||||
entry: bash -lc 'cd codex-rs && RUSTFLAGS="-D warnings" cargo build --workspace --locked --all-targets && cargo build -p codex-linux-sandbox --locked --all-targets'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
require_serial: true
|
||||
32
AGENTS.md
32
AGENTS.md
@@ -1,5 +1,31 @@
|
||||
# Rust/codex-rs
|
||||
# AGENTS.md
|
||||
|
||||
In the codex-rs folder where the rust code lives:
|
||||
This file provides guidance to OpenAI Codex (openai.com/codex) when working with
|
||||
code in this repository.
|
||||
|
||||
- Never add or modify any code related to `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR`. You operate in a sandbox where `CODEX_SANDBOX_NETWORK_DISABLED=1` will be set whenever you use the `shell` tool. Any existing code that uses `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` was authored with this fact in mind. It is often used to early exit out of tests that the author knew you would not be able to run given your sandbox limitations.
|
||||
## Build, Lint & Test
|
||||
|
||||
### JavaScript/TypeScript
|
||||
- Install dependencies: `pnpm install`
|
||||
- Run all tests: `pnpm test`
|
||||
- Run a single test: `pnpm test -- -t <pattern>` or `pnpm test -- path/to/file.spec.ts`
|
||||
- Watch tests: `pnpm test:watch`
|
||||
- Lint: `pnpm lint && pnpm lint:fix`
|
||||
- Type-check: `pnpm typecheck`
|
||||
- Format: `pnpm format:fix`
|
||||
- Build: `pnpm build`
|
||||
|
||||
### Rust (codex-rs workspace)
|
||||
- Build: `cargo build --workspace --locked`
|
||||
- Test all: `cargo test --workspace`
|
||||
- Test crate: `cargo test -p <crate>`
|
||||
- Single test: `cargo test -p <crate> -- <test_name>`
|
||||
- Format & check: `cargo fmt --all -- --check`
|
||||
- Lint: `cargo clippy --all-targets --all-features -- -D warnings`
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
- JS/TS: ESLint + Prettier; group imports; camelCase vars & funcs; PascalCase types/components; catch specific errors
|
||||
- Rust: rustfmt & Clippy (see `codex-rs/rustfmt.toml`); snake_case vars & funcs; PascalCase types; prefer early return; avoid `unwrap()` in prod
|
||||
- General: Do not swallow exceptions; use DRY; generate/validate ASCII art programmatically
|
||||
- Include any Cursor rules from `.cursor/rules/` or Copilot rules from `.github/copilot-instructions.md` if present
|
||||
|
||||
14
README.md
14
README.md
@@ -325,6 +325,20 @@ pnpm link
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Rust / Cargo (codex-rs)</strong></summary>
|
||||
|
||||
```bash
|
||||
# Ensure you have Rust and Cargo installed (via rustup)
|
||||
cd codex-rs/cli
|
||||
cargo install --path . --locked
|
||||
|
||||
# Or run without installing:
|
||||
cargo run --manifest-path codex-rs/cli/Cargo.toml -- --help
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Configuration guide
|
||||
|
||||
174
agentydragon/CHANGES.md
Normal file
174
agentydragon/CHANGES.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# codex-rs: Changes between HEAD and main
|
||||
|
||||
This document summarizes new and removed features, configuration options,
|
||||
and behavioral changes in the `codex-rs` workspace between the `main`
|
||||
branch and the current `HEAD`. Only additions/deletions (not unmodified
|
||||
code) are listed, with examples of usage and configuration.
|
||||
|
||||
---
|
||||
|
||||
## CLI Enhancements
|
||||
|
||||
### Build & Install from Source
|
||||
|
||||
```shell
|
||||
cargo install --path cli --locked
|
||||
# install system-wide:
|
||||
sudo cargo install --path cli --locked --root /usr/local
|
||||
```
|
||||
|
||||
### New `codex config` Subcommand
|
||||
|
||||
Manage your `~/.codex/config.toml` directly without manually editing:
|
||||
|
||||
```shell
|
||||
codex config edit # open config in $EDITOR (or vi)
|
||||
codex config set KEY VALUE # set a TOML literal, e.g. tui.auto_mount_repo true
|
||||
```
|
||||
|
||||
### New `codex inspect-env` Command
|
||||
|
||||
Inspect the sandbox/container environment (mounts, permissions, network):
|
||||
|
||||
```shell
|
||||
codex inspect-env --full-auto
|
||||
codex inspect-env -s network=disable -s mount=/mydir=rw
|
||||
```
|
||||
|
||||
### Resume TUI Sessions by UUID
|
||||
|
||||
```shell
|
||||
codex session <SESSION_UUID>
|
||||
```
|
||||
|
||||
### MCP Server (JSON‑RPC) Support
|
||||
|
||||
Launch Codex as an MCP _server_ over stdin/stdout and speak the
|
||||
Model Context Protocol (JSON-RPC):
|
||||
|
||||
```shell
|
||||
npx @modelcontextprotocol/inspector codex mcp
|
||||
```
|
||||
|
||||
#### Sample JSON‑RPC Interaction
|
||||
|
||||
```jsonc
|
||||
// ListTools request
|
||||
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }
|
||||
|
||||
// CallTool request
|
||||
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
|
||||
"params": { "name": "codex", "arguments": { "prompt": "Hello" } }
|
||||
}
|
||||
|
||||
// CallTool response (abbreviated)
|
||||
{ "jsonrpc": "2.0", "id": 2, "result": {
|
||||
"content": [ { "type": "text", "text": "Hi there", "annotations": null } ],
|
||||
"is_error": false
|
||||
}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Changes
|
||||
|
||||
### `auto_allow` Predicate Scripts
|
||||
|
||||
Automatically approve or deny shell commands via custom scripts:
|
||||
|
||||
```toml
|
||||
[[auto_allow]]
|
||||
script = "/path/to/approve_predicate.sh"
|
||||
[[auto_allow]]
|
||||
script = "my_predicate --flag"
|
||||
```
|
||||
|
||||
Vote resolution:
|
||||
- A `deny` vote aborts execution.
|
||||
- An `allow` vote auto-approves.
|
||||
- Otherwise falls back to manual approval prompt.
|
||||
|
||||
### `base_instructions_override`
|
||||
|
||||
Override or disable the built-in system prompt (`prompt.md`):
|
||||
|
||||
```bash
|
||||
export CODEX_BASE_INSTRUCTIONS_FILE=custom_prompt.md # use custom prompt
|
||||
export CODEX_BASE_INSTRUCTIONS_FILE="" # disable base prompt
|
||||
```
|
||||
|
||||
### TUI Configuration Options
|
||||
|
||||
In `~/.codex/config.toml`, under the `[tui]` table:
|
||||
|
||||
```toml
|
||||
editor = "${VISUAL:-${EDITOR:-nvim}}" # external editor for prompt
|
||||
message_spacing = true # insert blank line between messages
|
||||
sender_break_line = true # sender label on its own line
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Library Updates
|
||||
|
||||
### System Prompt Composition Customization
|
||||
|
||||
System messages now combine:
|
||||
1. Built-in prompt (`prompt.md`),
|
||||
2. User instructions (`AGENTS.md`/`instructions.md`),
|
||||
3. `apply-patch` tool instructions (for GPT-4.1),
|
||||
4. User command/prompt.
|
||||
|
||||
Controlled via `CODEX_BASE_INSTRUCTIONS_FILE`.
|
||||
|
||||
### Chat Completions Tool Call Buffering
|
||||
|
||||
User turns emitted during an in-flight tool invocation are buffered
|
||||
and flushed after the tool result, preventing interleaved messages.
|
||||
|
||||
### SandboxPolicy API Extensions
|
||||
|
||||
```rust
|
||||
policy.allow_disk_write_folder("/path/to/folder".into());
|
||||
policy.revoke_disk_write_folder("/path/to/folder");
|
||||
```
|
||||
|
||||
### Auto‑Approval Predicate Engine
|
||||
|
||||
```rust
|
||||
use codex_core::safety::{evaluate_auto_allow_predicates, AutoAllowVote};
|
||||
let vote = evaluate_auto_allow_predicates(&cmd, &config.auto_allow);
|
||||
match vote {
|
||||
AutoAllowVote::Allow => /* auto-approve */,
|
||||
AutoAllowVote::Deny => /* reject */,
|
||||
AutoAllowVote::NoOpinion => /* prompt user */,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TUI Improvements
|
||||
|
||||
### Double Ctrl+D Exit Confirmation
|
||||
|
||||
Prevent accidental exits by requiring two Ctrl+D within a timeout:
|
||||
|
||||
```rust
|
||||
use codex_tui::confirm_ctrl_d::ConfirmCtrlD;
|
||||
let mut confirm = ConfirmCtrlD::new(require_double, timeout_secs);
|
||||
// confirm.handle(now) returns true to exit, false to prompt confirmation
|
||||
```
|
||||
|
||||
### Markdown & Header Compact Rendering
|
||||
|
||||
New rendering options (code-level) for more compact chat layout:
|
||||
- `markdown_compact`
|
||||
- `header_compact`
|
||||
|
||||
---
|
||||
|
||||
## Documentation & Tests
|
||||
|
||||
- `codex-rs/config.md`, `codex-rs/README.md`, `core/README.md` updated with examples.
|
||||
- New `core/init.md` guidance for generating `AGENTS.md` templates.
|
||||
- Added tests for `codex config`, `ConfirmCtrlD`, and `evaluate_auto_allow_predicates`.
|
||||
87
agentydragon/README.md
Normal file
87
agentydragon/README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# agentydragon
|
||||
|
||||
This file documents the changes introduced on the `agentydragon` branch
|
||||
(off the `main` branch) of the codex repository.
|
||||
|
||||
## codex-rs: session resume and playback
|
||||
- Added `session` subcommand to the CLI (`codex session <UUID>`) to resume TUI sessions by UUID.
|
||||
- Integrated the `uuid` crate for session identifiers.
|
||||
- Updated TUI (`codex-rs/tui`) to respect and replay previous session transcripts:
|
||||
- Methods: `set_session_id`, `session_id`, `replay_items`.
|
||||
- Load rollouts from `sessions/rollout-<UUID>.jsonl`.
|
||||
- Printed resume command on exit: `codex session <UUID>`.
|
||||
|
||||
## codex-core enhancements
|
||||
- Exposed core model types: `ContentItem`, `ReasoningItemReasoningSummary`, `ResponseItem`.
|
||||
- Added `composer_max_rows` setting (with serde default) to TUI configuration.
|
||||
|
||||
## Dependency updates
|
||||
- Added `uuid` crate to `codex-rs/cli` and `codex-rs/tui`.
|
||||
|
||||
## Pre-commit config changes
|
||||
- Configured Rust build hook in `.pre-commit-config.yaml` to fail on warnings by setting `RUSTFLAGS="-D warnings"`.
|
||||
|
||||
## codex-rs/tui: Undo feedback decision with Esc key
|
||||
- Pressing `Esc` in feedback-entry mode now cancels feedback entry and returns to the select menu, preserving the partially entered feedback text.
|
||||
- Added a unit test for the ESC cancellation behavior in `tui/src/user_approval_widget.rs`.
|
||||
|
||||
## codex-rs/tui: restore inline mount DSL and slash-command dispatch
|
||||
- Reintroduced logic in `ChatComposer` to dispatch `AppEvent::InlineMountAdd` and `AppEvent::InlineMountRemove` when `/mount-add` or `/mount-remove` is entered with inline arguments.
|
||||
- Restored dispatch of `AppEvent::DispatchCommand` for slash commands selected via the command popup, including proper cleanup of the composer input.
|
||||
|
||||
## codex-rs/tui: slash-command `/edit-prompt` opens external editor
|
||||
- Fixed slash-command `/edit-prompt` to invoke the configured external editor for prompt drafting (in addition to Ctrl+E).
|
||||
|
||||
## codex-rs/tui: display context remaining percentage
|
||||
- Added module `tui/src/context.rs` with heuristics (`approximate_tokens_used`, `max_tokens_for_model`, `calculate_context_percent_remaining`).
|
||||
- Updated `ChatWidget` and `ChatComposer::render_ref` to track history items and render `<N>% context left` indicator with color thresholds.
|
||||
- Added unit tests in `tui/tests/context_percent.rs` for token counting and percent formatting boundary conditions.
|
||||
|
||||
## codex-rs/tui: compact Markdown rendering option
|
||||
- Added `markdown_compact` config flag under UI settings to collapse heading-content spacing when enabled.
|
||||
- When enabled, headings render immediately adjacent to content with no blank line between them.
|
||||
- Updated Markdown rendering in chat UI and logs to honor compact mode globally (diffs, docs, help messages).
|
||||
- Added unit tests covering H1–H6 heading spacing for both compact and default modes.
|
||||
## codex-rs: document MCP servers example in README
|
||||
- Added an inline TOML snippet under “Model Context Protocol Support” in `codex-rs/README.md` showing how to configure external `mcp_servers` entries in `~/.codex/config.toml`.
|
||||
- Documented `codex mcp` behavior: JSON-RPC over stdin/stdout, optional sandbox, no ephemeral container, default `codex` tool schema, and example ListTools/CallTool schema.
|
||||
|
||||
## Documentation tasks
|
||||
|
||||
## codex-rs/tui: interactive shell-command affordance via hotkey
|
||||
- Bound `Ctrl+M` to open a ShellCommandView overlay for arbitrary container shell input.
|
||||
- Toggled shell-command mode with `Ctrl+M` to enter or exit prompt, with styled border in shell mode.
|
||||
- Executed commands asynchronously (`sh -c`) and recorded outputs inline in conversation history.
|
||||
- Added unit tests for ShellCommandView event emission and shell-mode toggling behavior.
|
||||
|
||||
Tasks live under `agentydragon/tasks/` as individual Markdown files. Please update each task’s **Status** and **Implementation** sections in place rather than maintaining a static list here.
|
||||
|
||||
### Branch & Worktree Workflow
|
||||
|
||||
- **Branch convention**: work on each task in its own branch named `agentydragon-<task-id>-<task-slug>`, to avoid refname conflicts.
|
||||
- **Worktree helper**: in `agentydragon/tasks/`, run:
|
||||
-
|
||||
- ```sh
|
||||
- # Accept a full slug (NN-slug) or two-digit task ID (NN), optionally multiple; --tmux opens each in its own tmux pane and auto-commits each task as its Developer agent finishes:
|
||||
- agentydragon/tools/create_task_worktree.py [--agent] [--tmux] [--interactive] [--shell] [--skip-presubmit] <task-slug|NN> [<task-slug|NN>...]
|
||||
- ```
|
||||
-
|
||||
- Without `--agent`, this creates or reuses a worktree at
|
||||
- `agentydragon/tasks/.worktrees/<task-id>-<task-slug>` off the `agentydragon` branch.
|
||||
- Internally, the helper uses CoW hydration instead of a normal checkout: it registers the worktree with `git worktree add --no-checkout`, then performs a filesystem-level reflink
|
||||
- of all files (macOS: `cp -cRp`; Linux: `cp --reflink=auto`), falling back to `rsync` if reflinks aren’t supported. This makes new worktrees appear nearly instantly on supported filesystems while
|
||||
- preserving untracked files.
|
||||
- With `--agent`, after setting up a new worktree it runs presubmit pre-commit checks (aborting with a clear message on failure unless `--skip-presubmit` is passed), then launches the Developer Codex agent (using `prompts/developer.md` and the task file).
|
||||
- After the Developer agent exits, if the task’s **Status** is set to `Done`, it automatically runs the Commit agent helper to stage fixes and commit the work.
|
||||
**Commit agent helper**: in `agentydragon/tasks/`, run:
|
||||
|
||||
```sh
|
||||
# Generate and apply commit(s) for completed task(s) in their worktrees:
|
||||
agentydragon/tools/launch_commit_agent.py <task-slug|NN> [<task-slug|NN>...]
|
||||
```
|
||||
|
||||
After the Developer agent finishes and updates the task file, the Commit agent will write the commit message to a temporary file and then commit using that file (`git commit -F`). An external orchestrator can then stage files and run pre-commit hooks as usual. You do not need to run `git commit` manually.
|
||||
|
||||
---
|
||||
|
||||
*This README was autogenerated to summarize changes on the `agentydragon` branch.*
|
||||
38
agentydragon/WORKFLOW.md
Normal file
38
agentydragon/WORKFLOW.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Agent Handoff Workflow
|
||||
|
||||
This document explains the multi-agent handoff pattern used for task development and commits
|
||||
in the `agentydragon` workspace. It consolidates shared guidance so individual agent prompts
|
||||
do not need to repeat these details.
|
||||
|
||||
## 1. Developer Agent
|
||||
- **Scope**: Runs inside a sandboxed git worktree for a single task branch (`agentydragon-<ID>-<slug>`).
|
||||
- **Actions**:
|
||||
1. If the task’s **Status** is `Needs input`, stop immediately and await further instructions; do **not** implement code changes or run pre-commit hooks.
|
||||
2. Update the task Markdown file’s **Status** to `Done` when implementation is complete.
|
||||
3. Implement the code changes for the task.
|
||||
4. Run `pre-commit run --files $(git diff --name-only)` to apply and stage any autofix changes.
|
||||
5. **Do not** run `git commit`.
|
||||
|
||||
## 2. Commit Agent
|
||||
- **Scope**: Runs in the sandbox (read-only `.git`) or equivalent environment.
|
||||
- **Actions**:
|
||||
1. Emit exactly one line to stdout: the commit message prefixed `agentydragon(tasks): `
|
||||
summarizing the task’s **Implementation** section.
|
||||
2. Stop immediately.
|
||||
|
||||
## 3. Orchestrator
|
||||
- **Scope**: Outside the sandbox with full Git permissions.
|
||||
- **Actions**:
|
||||
1. Stage all changes: `git add -u`.
|
||||
2. Run `pre-commit run --files $(git diff --name-only --cached)`.
|
||||
3. Read the commit message and run `git commit -m "$MSG"`.
|
||||
|
||||
## 4. Status & Launch
|
||||
- Use `agentydragon_task.py status` to view tasks (including those in `.done/`).
|
||||
- Summaries:
|
||||
- **Merged:** tasks with no branch/worktree.
|
||||
- **Ready to merge:** tasks marked Done with branch commits ahead.
|
||||
- **Unblocked:** tasks with no outstanding dependencies.
|
||||
- The script also prints a `agentydragon/tools/create_task_worktree.py --agent --tmux <IDs>` command for all unblocked tasks.
|
||||
|
||||
This guide centralizes the handoff workflow for all agents.
|
||||
16
agentydragon/prompts/commit.md
Normal file
16
agentydragon/prompts/commit.md
Normal file
@@ -0,0 +1,16 @@
|
||||
## Commit Agent Prompt
|
||||
|
||||
Refer to `agentydragon/WORKFLOW.md` for the overall Developer→Commit→Orchestrator handoff workflow.
|
||||
|
||||
You are the **Commit** Codex agent for the `codex` repository. Your job is to stage and commit the changes made by the Developer agent.
|
||||
Your sole responsibility is to generate the Git commit message on stdout.
|
||||
Do **not** modify any files or run Git commands; this agent must remain sandbox-friendly.
|
||||
|
||||
When you run, **output exactly** the desired commit message (with no extra commentary) on stdout. The message must:
|
||||
- Be prefixed with `agentydragon(tasks): `
|
||||
- Concisely summarize the work performed as described in the task’s **Implementation** section.
|
||||
|
||||
Stop immediately after emitting the commit message. An external orchestrator will stage, run hooks, and commit using this message.
|
||||
|
||||
Below, you will get the task description the agent got. But still verify that the agent actually did what it was supposed to, and adjust the commit message according to what is actually implemented, DO NOT just copy what's in the task file.
|
||||
|
||||
24
agentydragon/prompts/developer.md
Normal file
24
agentydragon/prompts/developer.md
Normal file
@@ -0,0 +1,24 @@
|
||||
## Developer Agent Prompt
|
||||
|
||||
Refer to `agentydragon/WORKFLOW.md` for the overall Developer→Commit→Orchestrator handoff workflow.
|
||||
|
||||
You are the **Developer** Codex agent for the `codex` repository. You are running inside a dedicated git worktree for a single task branch.
|
||||
Use the task Markdown file under `agentydragon/tasks/` as your progress tracker: update its **Status** and **Implementation** sections to record your progress.
|
||||
|
||||
Before making any changes, read the task definition in `agentydragon/tasks/` and note that its **Status** and **Implementation** sections are placeholders.
|
||||
|
||||
After reviewing, update the task’s **Status** to "In progress" and fill in the **Implementation** section with your planned approach.
|
||||
If the **Implementation** section is blank or does not describe your intended design and steps, populate it with a concise high‑level plan before proceeding.
|
||||
Then proceed directly to implement the full functionality in the codebase as a single atomic unit—regardless of how many components are involved, do not split the work into separate sub-steps or pause to ask whether to decompose it.
|
||||
|
||||
Do not pause to seek user confirmation after editing the Markdown;
|
||||
only ask clarifying questions if you encounter genuine ambiguities in the requirements.
|
||||
|
||||
At any point, you may set the task’s **Status** to any valid state (e.g. Not started, In progress, Needs input, Needs manual review, Done, Cancelled) as appropriate. Use **Needs input** to request further clarification or resources before proceeding.
|
||||
|
||||
When you have finished working on the task file:
|
||||
- If the task’s **Status** is "Needs input", stop immediately and await further instructions; do **not** run pre-commit hooks or invoke the Commit agent.
|
||||
- Otherwise, set the task’s **Status** to "Done".
|
||||
- Run the repository’s pre-commit hooks on all changed files (e.g. `pre-commit run --files <changed-files>`), and stage any autofix changes.
|
||||
- Do **not** stage or commit beyond hook-driven fixes. Instead, stop and await the Commit agent to record your updates.
|
||||
Then stop and await further instructions.
|
||||
54
agentydragon/prompts/manager.md
Normal file
54
agentydragon/prompts/manager.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Project Manager Agent Prompt
|
||||
|
||||
You are the **Project Manager** Codex agent for the `codex` repository.
|
||||
Refer to `agentydragon/WORKFLOW.md` for the standard Developer→Commit→Orchestrator handoff workflow.
|
||||
Your responsibilities include:
|
||||
|
||||
- **Reading documentation**: Load and understand all relevant docs in this repo (especially those defining task, worktree, and branch conventions, as well as each task file and top‑level README files).
|
||||
- **Task orchestration**: Maintain the list of tasks, statuses, and dependencies; plan waves of work; and generate commands to launch work in parallel using `agentydragon/tools/create_task_worktree.py` (or the legacy `agentydragon/tools/create-task-worktree.sh`) with `--agent` and `--tmux`.
|
||||
- **Task creation**: When creating a new task stub, review the descriptions of all existing tasks; set the `dependencies` front-matter field to list the tasks that must be completed before work on this task can begin; and include a brief rationale as a Markdown comment (e.g., `<!-- rationale: depends on tasks X and Y because ... -->`) explaining why these dependencies are required and why other tasks are not.
|
||||
- **Live coordination**: Continuously monitor and report progress, adjust the plan as tasks complete or new ones appear, and surface any blockers.
|
||||
|
||||
- **Worktree monitoring**: Check each task’s worktree for uncommitted changes or dirty state to detect agents still working or potential crashes, and report their status as in-progress or needing attention.
|
||||
- When displaying the task-status table, highlight dirty worktrees in red and tasks marked Done or Merged in green; exclude tasks that are Merged with no branch and no worktree from the main table (they should instead be listed in a green “Done & merged:” summary at the bottom), and filter such merged tasks out of other tasks’ dependency lists.
|
||||
|
||||
- **Background polling**: On user request, enter a sleep‑and‑scan loop (e.g. 5 min interval) to detect tasks marked “Done” in their Markdown; for each completed task, review its branch worktree, check for merge conflicts, propose merging cleanly mergeable branches, and suggest conflict‑resolution steps for any that aren’t cleanly mergeable.
|
||||
- **Manager utilities**: Create and maintain utility scripts under `agentydragon/tools/manager_utils/` to support your work (e.g., branch scanning, conflict checking, merge proposals, polling loops). Include clear documentation (header comments or docstrings with usage examples) in each script, and invoke these scripts in your workflow.
|
||||
- **Merge orchestration**: When proposing merges of completed task branches into the integration branch, consider both single-branch and octopus (multi-branch) merges. Detect and report conflicts between branches as well as with the integration branch, and recommend resolution steps or merge ordering to avoid or resolve conflicts.
|
||||
|
||||
### First Actions
|
||||
|
||||
1. For each task branch (named `agentydragon-<task-id>-<task-slug>`), **without changing the current working directory’s Git HEAD or modifying its status**, create or open a dedicated worktree for that branch (e.g. via `agentydragon/tools/create_task_worktree.py <task-slug>`) and read the task’s Markdown copy in that worktree to extract and list the task number, title, live **Status**, and dependencies. *(Always read the **Status** and dependencies from the copy of the task file in the branch’s worktree, never from master/HEAD.)*
|
||||
2. Produce a one‑line tmux launch command to spin up only those tasks whose dependencies are satisfied and can actually run in parallel, following the conventions defined in repository documentation.
|
||||
3. Describe the high‑level wave‑by‑wave plan and explain which tasks can run in parallel.
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```bash
|
||||
# Parallel worktree launch
|
||||
agentydragon/tools/create_task_worktree.py --agent --tmux 02 04 07
|
||||
|
||||
# Wave-by-wave plan
|
||||
# Wave 1: tasks 02,04 (no unmet deps)
|
||||
# Wave 2: task 07 (depends on 02,04)
|
||||
|
||||
# Background polling loop (every 5 min)
|
||||
while true; do
|
||||
python3 agentydragon/tools/check_tasks.py && \
|
||||
python3 agentydragon/tools/launch_commit_agent.py $(python3 agentydragon/tools/find_done_tasks.py)
|
||||
sleep 300
|
||||
done
|
||||
|
||||
# Dispose a task worktree
|
||||
python3 agentydragon/tools/manager_utils/agentydragon_task.py dispose 07
|
||||
```
|
||||
|
||||
More functionality and refinements will be added later. Begin by executing these steps and await further instructions.
|
||||
|
||||
*If instructed, enter a background polling loop (sleep for a configured interval, e.g. 5 minutes) to watch for tasks whose Markdown status is updated to “Done” and then prepare review/merge steps for only those branches.*
|
||||
|
||||
Once a task branch is merged cleanly into the integration branch, dispose of its worktree and delete its Git branch. To record that merge, use:
|
||||
|
||||
python3 agentydragon/tools/manager_utils/agentydragon_task.py set-status <task-id> Merged
|
||||
|
||||
Use `python3 agentydragon/tools/manager_utils/agentydragon_task.py dispose <task-id>` to remove the worktree and branch without changing the status (e.g. for cancelled tasks).
|
||||
5
agentydragon/prompts/master-diff.md
Normal file
5
agentydragon/prompts/master-diff.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Read the full diff between HEAD and main and produce a list of everything that was added/removed.
|
||||
Include examples of how to use the features, how to configure them, etc.
|
||||
Use Markdown format. Write into $(git rev-parse --show-toplevel)/agentydragon/CHANGES.md. Delete it if it already exists.
|
||||
Only document changes under codex-rs.
|
||||
Do not include things that already exist on main branch - only what was changed.
|
||||
4
agentydragon/prompts/redepend.md
Normal file
4
agentydragon/prompts/redepend.md
Normal file
@@ -0,0 +1,4 @@
|
||||
read the description of all tasks in agentydragon/tasks/*.md and relevant context in codex-rs. for every task: disregard existing dependecy declarations in the frontmatter. think long about
|
||||
why and how they might depend on each other and if there's any way they might conflict and whether the overall picturen of how they fit toether makes sense. for each, *REGENERATE* the
|
||||
dependency list in frontmatter to the list of tasks the muast be done before each gvien taks becomes unblocked. no need to populate this for already merged tasks. also no need to list
|
||||
merged tasks inside any dependency list.
|
||||
66
agentydragon/prompts/scaffolding-setup.md
Normal file
66
agentydragon/prompts/scaffolding-setup.md
Normal file
@@ -0,0 +1,66 @@
|
||||
You are the AI “Scaffolding Assistant” for the `codex` monorepo. Your mission is to generate, in separate commits, all of the initial scaffolding needed for the
|
||||
tydragon-driven task workflow:
|
||||
|
||||
1. **Task stubs**
|
||||
- Create `agentydragon/tasks/task-template.md`.
|
||||
- Create numbered task stubs (`01-*.md`, `02-*.md`, …) for each planned feature (mounting, approval predicates, live‑reload, editor integration, etc.), filling in
|
||||
e, “Status”, “Goal”, and sections for “Acceptance Criteria”, “Implementation”, and “Notes”.
|
||||
|
||||
2. **Worktree launcher**
|
||||
- Implement `agentydragon/tools/create_task_worktree.py` with:
|
||||
- `--agent` mode to spin up a Codex agent in the worktree,
|
||||
- `--tmux` to tile panes for multiple tasks in a single tmux session,
|
||||
- two‑digit or slug ID resolution.
|
||||
- Ensure usage, help text, and numeric/slug handling are correct.
|
||||
|
||||
3. **Helper scripts**
|
||||
- Add `agentydragon/tasks/review-unmerged-task-branches.sh` to review and merge task branches.
|
||||
- Add `agentydragon/tools/launch-project-manager.sh` to invoke the Project Manager agent prompt.
|
||||
|
||||
4. **Project‑manager prompts**
|
||||
- Create `agentydragon/prompts/manager.md` containing the following Project Manager agent prompt:
|
||||
|
||||
```
|
||||
# Project Manager Agent Prompt
|
||||
|
||||
You are the **Project Manager** Codex agent for the `codex` repository. Your responsibilities include:
|
||||
|
||||
- **Reading documentation**: Load and understand all relevant docs in this repo (especially those defining task, worktree, and branch conventions, as well as each task file and top‑level README files).
|
||||
- **Task orchestration**: Maintain the list of tasks, statuses, and dependencies; plan waves of work; and generate shell commands to launch work on tasks in parallel using `create_task_worktree.py` with `--agent` and `--tmux`.
|
||||
- **Live coordination**: Continuously monitor and report progress, adjust the plan as tasks complete or new ones appear, and surface any blockers.
|
||||
- **Worktree monitoring**: Check each task’s worktree for uncommitted changes or dirty state to detect agents still working or potential crashes, and report their status as in-progress or needing attention.
|
||||
- **Background polling**: On user request, enter a sleep‑and‑scan loop (e.g. 5 min interval) to detect tasks marked “Done” in their Markdown; for each completed task, review its branch worktree, check for merge conflicts, propose merging cleanly mergeable branches, and suggest conflict‑resolution steps for any that aren’t cleanly mergeable.
|
||||
- **Manager utilities**: Create and maintain utility scripts under `agentydragon/tools/manager_utils/` to support your work (e.g., branch scanning, conflict checking, merge proposals, polling loops). Include clear documentation (header comments or docstrings with usage examples) in each script, and invoke these scripts in your workflow.
|
||||
- **Merge orchestration**: When proposing merges of completed task branches into the integration branch, consider both single-branch and octopus (multi-branch) merges. Detect and report conflicts between branches as well as with the integration branch, and recommend resolution steps or merge ordering to avoid or resolve conflicts.
|
||||
|
||||
### First Actions
|
||||
|
||||
1. For each task branch (named `agentydragon-<task-id>-<task-slug>`), **without changing the current working directory’s Git HEAD or modifying its status**, create or open a dedicated worktree for that branch (e.g. via `create_task_worktree.py <task-slug>`) and read the task’s Markdown copy under that worktree’s `agentydragon/tasks/` to extract and list the task number, title, live **Status**, and dependencies. *(Always read the **Status** and dependencies from the copy of the task file in the branch’s worktree, never from master/HEAD.)*
|
||||
2. Produce a one‑line tmux launch command to spin up only those tasks whose dependencies are satisfied and can actually run in parallel, following the conventions defined in repository documentation.
|
||||
3. Describe the high‑level wave‑by‑wave plan and explain which tasks can run in parallel.
|
||||
|
||||
More functionality and refinements will be added later. Begin by executing these steps and await further instructions.
|
||||
```
|
||||
|
||||
5. **Wave‑by‑wave plan**
|
||||
- Draft a human‑readable plan outlining task dependencies and four “waves” of work, indicating which tasks can run in parallel.
|
||||
|
||||
6. **Bootstrap commands**
|
||||
- Provide concrete shell/`rg`/`tmux` oneliner examples to launch Wave 1 (e.g. tasks 06, 03, 08) in parallel.
|
||||
- Provide a single tmux oneliner to spin up all unblocked tasks.
|
||||
|
||||
**Before you begin**, read the existing docs under `agentydragon/tasks/`, top‑level `README.md` and `oaipackaging/README.md` so you fully understand the context and
|
||||
entions.
|
||||
|
||||
**Commit strategy**
|
||||
- Commit each major component (tasks, script, helper scripts, prompts, plan) as its own Git commit.
|
||||
- Follow our existing commit-message style: prefix with `agentydragon(tasks):`, `agentydragon:`, etc.
|
||||
- Don’t batch everything into one huge commit; keep each logical piece isolated for easy review.
|
||||
|
||||
**Reporting**
|
||||
After each commit, print a short status message (e.g. “✅ Task stubs created”, “✅ create_task_worktree.py implemented”, etc.) and await confirmation before continuing
|
||||
the next step.
|
||||
|
||||
---
|
||||
|
||||
Begin now by listing the current task directory contents and generating `task-template.md`.
|
||||
1
agentydragon/tasks/.done/.gitkeep
Normal file
1
agentydragon/tasks/.done/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep this directory in version control
|
||||
64
agentydragon/tasks/.done/01-dynamic-mount-commands.md
Normal file
64
agentydragon/tasks/.done/01-dynamic-mount-commands.md
Normal file
@@ -0,0 +1,64 @@
|
||||
+++
|
||||
id = "01"
|
||||
title = "Dynamic Mount-Add and Mount-Remove Commands"
|
||||
status = "Merged"
|
||||
dependencies = ""
|
||||
last_updated = "2025-06-25T01:40:09.501150"
|
||||
+++
|
||||
|
||||
# Task 01: Dynamic Mount-Add and Mount-Remove Commands
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Merged
|
||||
**Summary**: Implemented inline DSL and interactive dialogs for `/mount-add` and `/mount-remove`, with dynamic sandbox policy updates.
|
||||
|
||||
## Goal
|
||||
Implement the `/mount-add` and `/mount-remove` slash commands in the TUI, supporting two modes:
|
||||
|
||||
1. **Inline DSL**: e.g. `/mount-add host=/path/to/host container=/path/in/agent mode=rw`
|
||||
2. **Interactive dialog**: if the user just types `/mount-add` or `/mount-remove` without args, pop up a prompt to fill in `host`, `container`, and optional `mode` fields.
|
||||
|
||||
These commands should:
|
||||
- Create or remove symlinks (or real directories) under the current working directory.
|
||||
- Update the in-memory `SandboxPolicy` to grant or revoke read/write permission for the host path.
|
||||
- Emit confirmation or error messages into the TUI log pane.
|
||||
|
||||
## Acceptance Criteria
|
||||
- Users can type `/mount-add host=... container=... mode=...` and the mount is created immediately.
|
||||
- Users can type `/mount-add` alone to open a small TUI form prompting for the three fields.
|
||||
- Symmetrically for `/mount-remove` by container path.
|
||||
- The `sandbox_policy` is updated so subsequent shell commands can read/write the newly mounted folder.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Added two new slash commands (`mount-add`, `mount-remove`) to the TUI’s `slash-command` popup.
|
||||
- Inline DSL parsing: commands typed as `/mount-add host=... container=... mode=...` or `/mount-remove container=...` are detected and handled immediately by parsing key/value args, performing the mount/unmount, and updating the `Config.sandbox_policy` in memory.
|
||||
- Interactive dialogs: selecting `/mount-add` or `/mount-remove` without args opens a bottom‑pane form (`MountAddView` or `MountRemoveView`) that prompts sequentially for the required fields and then triggers the same mount logic.
|
||||
- Mount logic implemented in `do_mount_add`/`do_mount_remove`:
|
||||
- Creates/removes a symlink under `cwd` pointing to the host path (`std::os::unix::fs::symlink` on Unix, platform equivalents on Windows).
|
||||
- Uses new `SandboxPolicy` methods (`allow_disk_write_folder`/`revoke_disk_write_folder`) to grant or revoke `DiskWriteFolder` permissions for the host path.
|
||||
- Emits success or error messages via `tracing::info!`/`tracing::error!`, which appear in the TUI log pane.
|
||||
|
||||
**How it works**
|
||||
1. **Inline DSL**
|
||||
- User types:
|
||||
```
|
||||
/mount-add host=/path/to/host container=path/in/cwd mode=ro
|
||||
```
|
||||
- The first-stage popup intercepts the mount-add command with args, dispatches `InlineMountAdd`, and the app parses the args and runs the mount logic immediately.
|
||||
2. **Interactive dialog**
|
||||
- User types `/mount-add` (or selects it via the popup) without args.
|
||||
- A small form appears that prompts for `host`, `container`, then `mode`.
|
||||
- Upon completion, the same mount logic runs.
|
||||
3. **Unmount**
|
||||
- `/mount-remove container=...` (inline) or `/mount-remove` (interactive) remove the symlink and revoke write permissions.
|
||||
4. **Policy update**
|
||||
- `allow_disk_write_folder` appends a `DiskWriteFolder` permission for new mounts.
|
||||
- `revoke_disk_write_folder` removes the corresponding permission on unmount.
|
||||
|
||||
## Notes
|
||||
- This builds on the static `[[sandbox.mounts]]` support introduced earlier.
|
||||
42
agentydragon/tasks/.done/03-live-config-reload.md
Normal file
42
agentydragon/tasks/.done/03-live-config-reload.md
Normal file
@@ -0,0 +1,42 @@
|
||||
+++
|
||||
id = "03"
|
||||
title = "Live Config Reload and Prompt on Changes"
|
||||
status = "Merged"
|
||||
dependencies = "02,07,09,11,14,29"
|
||||
last_updated = "2025-06-25T05:36:17.783726"
|
||||
+++
|
||||
|
||||
# Task 03: Live Config Reload and Prompt on Changes
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Done
|
||||
**Summary**: Live config watcher, diff prompt, and reload integration implemented.
|
||||
|
||||
## Goal
|
||||
Detect changes to the user `config.toml` file while a session is running and prompt the user to apply or ignore the updated settings.
|
||||
|
||||
## Acceptance Criteria
|
||||
- A background file watcher watches `$CODEX_HOME/config.toml` (or active user config path).
|
||||
- On any write event, compute a unified diff between the in-memory config and the on-disk file.
|
||||
- Pause the agent, display the diff in the TUI bottom pane, and offer two actions: `Apply new config now` or `Continue with old config`.
|
||||
- If the user applies, re-parse the config, merge overrides, and resume using the new settings. Otherwise, discard changes and resume.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Added `codex_tui::config_reload::generate_diff` to compute unified diffs via the `similar` crate (with a unit test).
|
||||
- Spawned a `notify`-based filesystem watcher thread in `tui::run_main` that debounces write events on `$CODEX_HOME/config.toml`, generates diffs against the last-read contents, and posts `AppEvent::ConfigReloadRequest(diff)`.
|
||||
- Introduced `AppEvent` variants (`ConfigReloadRequest`, `ConfigReloadApply`, `ConfigReloadIgnore`) and wired them in `App::run` to display a new `BottomPaneView` overlay.
|
||||
- Created `BottomPaneView` implementation `ConfigReloadView` to render the diff and handle `<Enter>`/`<Esc>` for apply or ignore.
|
||||
- On apply, reloaded `Config` via `Config::load_with_cli_overrides`, updated both `App.config` and `ChatWidget` (rebuilding its bottom pane with updated settings).
|
||||
|
||||
**How it works**
|
||||
- The watcher thread detects on-disk changes and pushes a diff request into the UI event loop.
|
||||
- Upon `ConfigReloadRequest`, the TUI bottom pane overlays the diff view and blocks normal input.
|
||||
- `<Enter>` applies the new config (re-parses and updates runtime state); `<Esc>` dismisses the overlay and continues with the old settings.
|
||||
|
||||
## Notes
|
||||
- Leverage a crate such as `notify` for FS events and `similar` or `diff` for unified diff generation.
|
||||
42
agentydragon/tasks/.done/06-external-editor-prompt.md
Normal file
42
agentydragon/tasks/.done/06-external-editor-prompt.md
Normal file
@@ -0,0 +1,42 @@
|
||||
+++
|
||||
id = "06"
|
||||
title = "External Editor Integration for Prompt Entry"
|
||||
status = "Merged"
|
||||
dependencies = "02,07,09,11,14,29"
|
||||
last_updated = "2025-06-25T02:40:09.505778"
|
||||
+++
|
||||
|
||||
# Task 06: External Editor Integration for Prompt Entry
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Done
|
||||
**Summary**: External editor integration for prompt entry implemented.
|
||||
|
||||
## Goal
|
||||
Allow users to spawn an external editor (e.g. Neovim) to compose or edit the chat prompt. The prompt box should update with the editor's contents when closed.
|
||||
|
||||
## Acceptance Criteria
|
||||
- A slash command `/edit-prompt` (or `Ctrl+E`) launches the user's preferred editor on a temporary file pre-populated with the current draft.
|
||||
- Upon editor exit, the draft is re-read into the composer widget.
|
||||
- Configurable via `editor = "${VISUAL:-${EDITOR:-nvim}}"` setting in `config.toml`.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Added `editor` option to `[tui]` section in `config.toml`, defaulting to `${VISUAL:-${EDITOR:-nvim}}`.
|
||||
- Exposed the `tui.editor` setting in the `codex-core` config model (`config_types.rs`) and wired it through to the TUI.
|
||||
- Added a new slash-command variant `EditPrompt` in `tui/src/slash_command.rs` to trigger external-editor mode.
|
||||
- Implemented `ChatComposer::open_external_editor()` in `tui/src/bottom_pane/chat_composer.rs`:
|
||||
- Creates a temporary file pre-populated with the current draft prompt.
|
||||
- Launches the configured editor (from `VISUAL`/`EDITOR` with `nvim` fallback) in a blocking subprocess.
|
||||
- Reads the edited contents back into the `TextArea` on editor exit.
|
||||
- Wired both `Ctrl+E` and the `/edit-prompt` slash command to invoke `open_external_editor()`.
|
||||
- Updated `config.md` to document the new `editor` setting under `[tui]`.
|
||||
|
||||
**How it works**
|
||||
- Pressing `Ctrl+E`, or typing `/edit-prompt` and hitting Enter, spawns the user's preferred editor on a temporary file containing the current draft.
|
||||
- When the editor process exits, the plugin reads back the file and updates the chat composer with the edited text.
|
||||
- The default editor is determined by `VISUAL`, then `EDITOR`, falling back to `nvim` if neither is set.
|
||||
37
agentydragon/tasks/.done/07-undo-feedback-decision.md
Normal file
37
agentydragon/tasks/.done/07-undo-feedback-decision.md
Normal file
@@ -0,0 +1,37 @@
|
||||
+++
|
||||
id = "07"
|
||||
title = "Undo Feedback Decision with Esc Key"
|
||||
status = "Merged"
|
||||
dependencies = "01,04,10,12,16,17"
|
||||
last_updated = "2025-06-25T01:40:09.506146"
|
||||
+++
|
||||
|
||||
# Task 07: Undo Feedback Decision with Esc Key
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Merged
|
||||
**Summary**: ESC key now cancels feedback entry and returns to the select menu, preserving any entered text; implementation and tests added.
|
||||
|
||||
## Goal
|
||||
Enhance the user-approval dialog so that if the user opted to leave feedback (“No, enter feedback”) they can press `Esc` to cancel the feedback flow and return to the previous approval choice menu (e.g. “Yes, proceed” vs. “No, enter feedback”).
|
||||
|
||||
## Acceptance Criteria
|
||||
- While the feedback-entry textarea is active, pressing `Esc` closes the feedback editor and reopens the yes/no confirmation dialog.
|
||||
- The cancellation must restore the dialog state without losing any partially entered feedback text.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- In `tui/src/user_approval_widget.rs`, updated `UserApprovalWidget::handle_input_key` so that pressing `Esc` in input mode switches `mode` back to `Select` (rather than sending a deny decision), and restores `selected_option` to the feedback entry item without clearing the input buffer.
|
||||
- Added a unit test in the same module to verify that `Esc` cancels input mode, preserves the feedback text, and does not emit any decision event.
|
||||
|
||||
**How it works**
|
||||
- When the widget is in `Mode::Input` (feedback-entry), receiving `KeyCode::Esc` resets `mode` to `Select` and sets `selected_option` to the index of the “Edit or give feedback” option.
|
||||
- The `input` buffer remains intact, so any partially typed feedback is preserved for if/when the user re-enters feedback mode.
|
||||
- No approval decision is sent on `Esc`, so the modal remains active and the user can still approve, deny, or re-enter feedback.
|
||||
|
||||
## Notes
|
||||
- Changes in `tui/src/user_approval_widget.rs` to treat `Esc` in input mode as a cancel-feedback action and added corresponding tests.
|
||||
52
agentydragon/tasks/.done/08-set-shell-title.md
Normal file
52
agentydragon/tasks/.done/08-set-shell-title.md
Normal file
@@ -0,0 +1,52 @@
|
||||
+++
|
||||
id = "08"
|
||||
title = "Set Shell Title to Reflect Session Status"
|
||||
status = "Merged"
|
||||
dependencies = "02,07,09,11,14,29"
|
||||
last_updated = "2025-06-25T04:06:55.265790"
|
||||
+++
|
||||
|
||||
# Task 08: Set Shell Title to Reflect Session Status
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Done
|
||||
**Summary**: Implemented session title persistence, `/set-title` slash command, and real-time ANSI updates in both TUI and exec clients.
|
||||
|
||||
## Goal
|
||||
|
||||
Allow the CLI to update the terminal title bar to reflect the current session status—executing, thinking (sampling), idle, or waiting for approval decision—and persist the title with the session. Users should also be able to explicitly set a custom title.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Implement a slash command or API (`/set-title <title>`) for users to explicitly set the session title.
|
||||
- Persist the title in session metadata so that on resume the last title is restored.
|
||||
- Dynamically update the shell/terminal title in real time based on session events:
|
||||
- Executing: use a play symbol (e.g. ▶)
|
||||
- Thinking/sampling: use an hourglass or brain symbol (e.g. ⏳)
|
||||
- Idle: use a green dot or sleep symbol (e.g. 🟢)
|
||||
- Waiting for approval decision: use an attention-grabbing symbol (e.g. ❗)
|
||||
- Ensure title updates work across Linux, macOS, and Windows terminals via ANSI escape sequences.
|
||||
|
||||
## Implementation
|
||||
**Note**: Populate this section with a concise high-level plan before beginning detailed implementation.
|
||||
|
||||
**Planned approach**
|
||||
- Extend the session protocol schema (`SessionConfiguredEvent`) in `codex-rs/core` to include an optional `title` field and introduce a new `SessionUpdatedTitleEvent` type.
|
||||
- Add a `SetTitle { title: String }` variant to the `Op` enum for custom titles and implement the `/set-title <text>` slash command in the TUI crates (`tui/src/slash_command.rs`, `tui/src/app_event.rs`, and `tui/src/app.rs`).
|
||||
- Modify the core agent loop to handle `Op::SetTitle`: persist the new title in session metadata, emit a `SessionUpdatedTitleEvent`, and include the persisted title in `SessionConfiguredEvent` on startup/resume.
|
||||
- Implement event listeners in both the interactive TUI (`tui/src/chatwidget.rs`) and non-interactive exec client (`exec/src/event_processor.rs`) that respond to session, title, and lifecycle events (session start, task begin/end, reasoning, idle, approval) by emitting ANSI escape sequences (`\x1b]0;<symbol> <title>\x07`) to update the terminal title bar.
|
||||
- Choose consistent Unicode symbols for each session state—executing (▶), thinking (⏳), idle (🟢), awaiting approval (❗)—and apply these as status indicators prefixed to the title.
|
||||
- On session startup or resume, restore the last persisted title or fall back to a default if none exists.
|
||||
|
||||
**How it works**
|
||||
- Users type `/set-title MyTitle` to set a custom session title; the core persists it and broadcasts a `SessionUpdatedTitleEvent`.
|
||||
- Clients print the appropriate ANSI escape code to update the terminal title before rendering UI or logs, reflecting real-time session state via the selected status symbol prefix.
|
||||
|
||||
## Notes
|
||||
|
||||
- Use ANSI escape code `\033]0;<title>\007` to set the terminal title.
|
||||
- Extend the session JSON schema to include a `title` field.
|
||||
- Select Unicode symbols that render consistently in common terminal fonts.
|
||||
52
agentydragon/tasks/.done/10-inspect-container-state.md
Normal file
52
agentydragon/tasks/.done/10-inspect-container-state.md
Normal file
@@ -0,0 +1,52 @@
|
||||
+++
|
||||
id = "10"
|
||||
title = "Inspect Container State (Mounts, Permissions, Network)"
|
||||
status = "Merged"
|
||||
dependencies = ""
|
||||
last_updated = "2025-06-25T04:07:56.197523"
|
||||
+++
|
||||
|
||||
# Task 10: Inspect Container State (Mounts, Permissions, Network)
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Completed
|
||||
**Summary**: Implemented `codex inspect-env` subcommand, CLI output and TUI bindings, tested in sandbox and headless modes.
|
||||
|
||||
## Goal
|
||||
|
||||
Provide a runtime command that displays the current sandbox/container environment details—what is mounted where, permission scopes, network access status, and other relevant sandbox policies.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Implement a slash command or CLI subcommand (`/inspect-env` or `codex inspect-env`) that outputs:
|
||||
- List of bind mounts (host path → container path, mode)
|
||||
- File-system permission policies in effect
|
||||
- Network sandbox status (restricted or allowed)
|
||||
- Runtime TUI status‑bar indicators for key sandbox attributes (e.g. network enabled/disabled, mount count, read/write scopes)
|
||||
- Any additional sandbox rules or policy settings applied
|
||||
- Format the output in a human-readable table or tree view in the TUI and plaintext for logs.
|
||||
- Ensure the command works in both interactive TUI sessions and non-interactive (headless) modes.
|
||||
- Include a brief explanation header summarizing each section to help users understand what they are seeing.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
Implemented a new `inspect-env` subcommand in `codex-cli`, reusing `create_sandbox_policy` and `Config::load_with_cli_overrides` to derive the effective sandbox policy and working directory. The code computes read-only or read-write mount entries (root and writable roots), enumerates granted `SandboxPermission`s, and checks `has_full_network_access()`. It then prints a formatted table (via `println!`) and summary counts.
|
||||
|
||||
**How it works**
|
||||
Running `codex inspect-env` loads user overrides, builds the sandbox policy, and:
|
||||
- Lists mounts (path and mode) in a table.
|
||||
- Prints each granted permission.
|
||||
- Shows network status as `enabled`/`disabled`.
|
||||
- Outputs summary counts for mounts and writable roots.
|
||||
|
||||
This command works both in CI/headless and inside the TUI (status-bar integration).
|
||||
|
||||
## Notes
|
||||
|
||||
- Leverage existing sandbox policy data structures used at startup.
|
||||
- Reuse TUI table or tree components for formatting (e.g., tui-rs widgets).
|
||||
- Include clear labels for network status (e.g., `NETWORK: disabled` or `NETWORK: enabled`).
|
||||
61
agentydragon/tasks/.done/11-custom-approval-predicates.md
Normal file
61
agentydragon/tasks/.done/11-custom-approval-predicates.md
Normal file
@@ -0,0 +1,61 @@
|
||||
+++
|
||||
id = "11"
|
||||
title = "User-Configurable Approval Predicates"
|
||||
status = "Merged"
|
||||
dependencies = "01,04,10,12,16,17"
|
||||
last_updated = "2025-06-25T01:40:09.508560"
|
||||
+++
|
||||
|
||||
# Task 11: User-Configurable Approval Predicates
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Merged
|
||||
**Summary**: Implemented custom approval predicates feature: configuration parsing, predicate invocation logic, tests, and documentation.
|
||||
|
||||
## Goal
|
||||
|
||||
Allow users to plug in an external executable that makes approval decisions for shell commands based on session context.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Support a new `[[approval_predicates]]` section in `config.toml` for Python-based predicates, each with a `python_predicate_binary = "..."` field (pointing to the predicate executable) and an implicit `never_expire = true` setting.
|
||||
- Before prompting the user, invoke each configured predicate in order, passing the following (via CLI args or env vars):
|
||||
- Session ID
|
||||
- Container working directory (CWD)
|
||||
- Host working directory (CWD)
|
||||
- Candidate shell command string
|
||||
- The predicate must print exactly one of `allow`, `deny`, or `ask` on stdout:
|
||||
- `allow` → auto-approve and skip remaining predicates
|
||||
- `deny` → auto-reject and skip remaining predicates
|
||||
- `ask` → open the standard approval dialog and skip remaining predicates
|
||||
- If a predicate exits non-zero or outputs anything else, treat it as `ask` and continue to the next predicate.
|
||||
- Write unit and integration tests covering typical and edge-case predicate behavior.
|
||||
- Document configuration syntax and behavior in the top-level config docs (`config.md`).
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Added `approval_predicates` field to `ConfigToml` and `Config` in `codex_core::config`, supporting a `python_predicate_binary: PathBuf` and an implicit `never_expire = true`.
|
||||
- Hooked into the command-approval code path in `codex_core::safety` to invoke each configured predicate executable before showing the approval prompt. Predicates are launched via `std::process::Command` with context passed in environment variables (`CODEX_SESSION_ID`, `CODEX_CONTAINER_CWD`, `CODEX_HOST_CWD`, `CODEX_COMMAND`).
|
||||
- Parsed each predicate’s stdout for exactly `allow`, `deny`, or `ask`, short-circuiting on `allow` or `deny` (auto-approve/auto-reject) and treating failures or unexpected output as `ask` to continue to the next predicate.
|
||||
- Wrote unit tests for configuration parsing and predicate-invocation behavior, covering exit-code and output edge cases, plus integration tests verifying end-to-end approval decisions.
|
||||
- Updated `config.md` to document the `[[approval_predicates]]` table syntax, default semantics, and runtime behavior.
|
||||
|
||||
**How it works**
|
||||
When a shell command requires approval, Codex iterates over each entry in `[[approval_predicates]]` in order. For each predicate:
|
||||
- Launch the configured binary with session context in its environment.
|
||||
- If it exits successfully and writes `allow`, Codex auto-approves and skips remaining predicates.
|
||||
- If it writes `deny`, Codex auto-rejects and skips remaining predicates.
|
||||
- Otherwise (writes `ask`, fails, or emits unexpected output), Codex moves to the next predicate or falls back to the manual approval dialog if none return `allow` or `deny`.
|
||||
This mechanism lets users automate approval decisions via custom Python scripts while retaining manual control when predicates defer.
|
||||
|
||||
## Notes
|
||||
|
||||
- Consider passing context via environment variables (e.g. `CODEX_SESSION_ID`, `CODEX_CONTAINER_CWD`, `CODEX_HOST_CWD`, `CODEX_COMMAND`).
|
||||
- Reuse invocation logic from the auto-approval predicates feature (Task 02).
|
||||
- **Motivating example**: auto-approve `pre-commit run --files <any number of space-separated files>`.
|
||||
- **Motivating example**: auto-approve any `git` command (e.g. `git add`, `git commit`, `git push`, `git status`, etc.) provided its repository root is under `<directory>`, correctly handling common flags and safe invocation modes.
|
||||
- **Motivating example**: auto-approve any shell pipeline composed out of `<these known-safe commands>` operating on `<known-safe files>` with `<known-safe params>`, using a general pipeline parser to ensure safety—a nontrivial example of predicate logic.
|
||||
@@ -0,0 +1,45 @@
|
||||
+++
|
||||
id = "13"
|
||||
title = "Interactive Prompting and Commands While Executing"
|
||||
status = "Merged"
|
||||
dependencies = "02,07,09,11,14,29"
|
||||
last_updated = "2025-06-25T01:40:09.509881"
|
||||
+++
|
||||
|
||||
# Task 13: Interactive Prompting and Commands While Executing
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Merged
|
||||
**Summary**: Implemented interactive prompt overlay allowing user input during streaming without aborting runs.
|
||||
|
||||
## Goal
|
||||
|
||||
Allow users to interleave composing prompts and issuing slash-commands while the agent is actively executing (e.g. streaming completions), without aborting the current run.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- While the LLM is streaming a response or executing a tool, the input box remains active for user edits and slash-commands.
|
||||
- Sending a message or `/`-command does not implicitly cancel or abort the ongoing execution.
|
||||
- Any tool invocation messages from the agent must still be immediately followed by their corresponding tool output messages (or the API will error).
|
||||
- Ensure the TUI correctly preserves the stream and appends new user input at the bottom, scrolling as needed.
|
||||
- No deadlocks or lost events if the agent finishes while the user is typing; buffer and render properly.
|
||||
- Update tests to simulate concurrent user input during streaming and validate UI state.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Modified `BottomPane::handle_key_event` in `tui/src/bottom_pane/mod.rs` to special-case the `StatusIndicatorView` while `is_task_running`, forwarding key events to `ChatComposer` and preserving the overlay.
|
||||
- Updated `BottomPane::render_ref` to always render the composer first and then overlay the active view, ensuring the input box remains visible and editable under the status indicator.
|
||||
- Added unit tests in `tui/src/bottom_pane/mod.rs` to verify input is forwarded during task execution and that the status indicator overlay is removed upon task completion.
|
||||
|
||||
**How it works**
|
||||
During LLM streaming or tool execution, the `StatusIndicatorView` remains active as an overlay. The modified event handler detects this overlay and forwards user key events to the underlying `ChatComposer` without dismissing the overlay. On task completion (`set_task_running(false)`), the overlay is automatically removed (via `should_hide_when_task_is_done`), returning to the normal input-only view.
|
||||
|
||||
## Notes
|
||||
|
||||
- Look at the ChatComposer and streaming loop in `tui/src/bottom_pane/chat_composer.rs` for input and stream handling.
|
||||
- Ensure event loop in `app.rs` multiplexes between agent stream events and user input events without blocking.
|
||||
- Consider locking or queuing tool-use messages to guarantee prompt tool-output pairing.
|
||||
@@ -0,0 +1,95 @@
|
||||
+++
|
||||
id = "15"
|
||||
title = "Agent Worktree Sandbox Configuration"
|
||||
status = "Merged"
|
||||
dependencies = "02,07,09,11,14,29"
|
||||
last_updated = "2025-06-25T07:26:13.570520"
|
||||
+++
|
||||
|
||||
# Task 15: Agent Worktree Sandbox Configuration
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Done
|
||||
**Summary**: Enhanced the task scaffolding script to launch a Codex agent in a sandboxed worktree with writable worktree and TMPDIR, auto-approved file I/O and Git operations, and network disabled.
|
||||
|
||||
## Goal
|
||||
|
||||
Use `create-task-worktree.sh --agent` to wrap the agent invocation in a sandbox with these properties:
|
||||
- The task worktree path and the system temporary directory (`$TMPDIR` or `/tmp`) are mounted read-write.
|
||||
- All other paths on the host are treated as read-only.
|
||||
- Git operations in the worktree (e.g. `git add`, `git commit`) succeed without additional confirmation.
|
||||
- Any file read or write under the worktree root is automatically approved.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
The `create-task-worktree.sh --agent` invocation:
|
||||
- launches the agent via `codex debug landlock` (or equivalent), passing flags to mount only the worktree and tempdir as writable.
|
||||
- sets up Landlock permissions so that all other host paths are read-only.
|
||||
- auto-approves any file system operation under the worktree directory.
|
||||
- auto-approves Git commands in the worktree without prompting.
|
||||
- still permits using system temp dir for ephemeral files.
|
||||
- contains tests or manual verifications demonstrating blocked writes outside and allowed writes inside.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Extended `create-task-worktree.sh` `--agent` mode to launch the Codex agent under a Landlock+seccomp sandbox by invoking `codex debug landlock --full-auto`, which grants write access only to the worktree (`cwd`) and the platform temp folder (`TMPDIR`), and disables network.
|
||||
- Updated the `-a|--agent` help text to reflect the new sandbox behavior and tempdir whitelist.
|
||||
- Added a test script demonstrating allowed writes inside the worktree and TMPDIR and blocked writes to directories outside those paths:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Test script for Task 15: verify sandbox restrictions and allowances
|
||||
set -euo pipefail
|
||||
|
||||
worktree_root="$(cd "$(dirname "$0")"/.. && pwd)"
|
||||
|
||||
echo "Running sandbox tests in worktree: $worktree_root"
|
||||
|
||||
# Test write inside worktree
|
||||
echo -n "Test: write inside worktree... "
|
||||
if codex debug landlock --full-auto /usr/bin/env bash -c "touch '$worktree_root/inside_test'"; then
|
||||
echo "PASS"
|
||||
else
|
||||
echo "FAIL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test write inside TMPDIR
|
||||
tmpdir=${TMPDIR:-/tmp}
|
||||
echo -n "Test: write inside TMPDIR ($tmpdir)... "
|
||||
if codex debug landlock --full-auto /usr/bin/env bash -c "touch '$tmpdir/tmp_test'"; then
|
||||
echo "PASS"
|
||||
else
|
||||
echo "FAIL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prepare external directory under HOME to test outside worktree/TMPDIR
|
||||
external_dir="$HOME/sandbox_test_dir"
|
||||
mkdir -p "$external_dir"
|
||||
rm -f "$external_dir/outside_test"
|
||||
|
||||
echo -n "Test: write outside allowed paths ($external_dir)... "
|
||||
if codex debug landlock --full-auto /usr/bin/env bash -c "touch '$external_dir/outside_test'"; then
|
||||
echo "FAIL: outside write succeeded" >&2
|
||||
exit 1
|
||||
else
|
||||
echo "PASS"
|
||||
fi
|
||||
```
|
||||
|
||||
**How it works**
|
||||
When invoked with `--agent`, `create-task-worktree.sh` changes into the task worktree and launches:
|
||||
|
||||
```bash
|
||||
codex debug landlock --full-auto codex "$(< \"$repo_root/agentydragon/prompts/developer.md\")"
|
||||
```
|
||||
|
||||
The `--full-auto` flag configures Landlock to allow disk writes under the current directory and the system temp directory, disable network access, and automatically approve commands on success. As a result, any file I/O and Git operations in the worktree proceed without approval prompts, while writes outside the worktree and TMPDIR are blocked by the sandbox.
|
||||
|
||||
## Notes
|
||||
|
||||
- This feature depends on the underlying Landlock/Seatbelt sandbox APIs.
|
||||
- Leverage the existing sandbox invocation (`codex debug landlock`) and approval predicates to auto-approve worktree and tmpdir I/O.
|
||||
54
agentydragon/tasks/.done/16-confirm-on-ctrl-d.md
Normal file
54
agentydragon/tasks/.done/16-confirm-on-ctrl-d.md
Normal file
@@ -0,0 +1,54 @@
|
||||
+++
|
||||
id = "16"
|
||||
title = "Confirm on Ctrl+D to Exit"
|
||||
status = "Merged"
|
||||
dependencies = ""
|
||||
last_updated = "2025-06-25T05:36:23.493497"
|
||||
+++
|
||||
|
||||
# Task 16: Confirm on Ctrl+D to Exit
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Done
|
||||
**Summary**: Double Ctrl+D confirmation implemented and tested.
|
||||
|
||||
## Goal
|
||||
|
||||
Require two consecutive Ctrl+D keystrokes (within a short timeout) to exit the TUI, preventing accidental termination from a single SIGINT.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Add a `[tui] require_double_ctrl_d = true` config flag (default `false`) to enable double‑Ctrl+D exit confirmation.
|
||||
- When `require_double_ctrl_d` is enabled:
|
||||
- First Ctrl+D within the TUI suspends exit and shows a status message like "Press Ctrl+D again to confirm exit".
|
||||
- If a second Ctrl+D occurs within a configurable timeout (e.g. 2 sec), the TUI exits normally.
|
||||
- If no second Ctrl+D arrives before timeout, clear the confirmation state and resume normal operation.
|
||||
- Ensure that child processes (shell tool calls) still receive SIGINT immediately and are not affected by the double‑Ctrl+D logic.
|
||||
- Prevent immediate exit on Ctrl+D (EOF); require the same double‑confirmation workflow as for Ctrl+D when EOF is received.
|
||||
- Provide unit or integration tests simulating SIGINT events to verify behavior.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Added `require_double_ctrl_d` and `double_ctrl_d_timeout_secs` to the TUI config in `core/src/config_types.rs` with defaults.
|
||||
- Introduced `ConfirmCtrlD` helper in `tui/src/confirm_ctrl_d.rs` to manage confirmation state and expiration logic.
|
||||
- Extended `App` in `tui/src/app.rs`:
|
||||
- Initialized `confirm_ctrl_d` from config in `App::new`.
|
||||
- Expired stale confirmation windows each event-loop tick and cleared the status overlay when timed out.
|
||||
- Replaced the Ctrl+D handler to invoke `ConfirmCtrlD::handle`, exiting only on confirmed press and otherwise displaying a prompt via `BottomPane`.
|
||||
- Leveraged `BottomPane::set_task_running(true)` and `update_status_text` to render the confirmation prompt overlay.
|
||||
- Added unit tests for `ConfirmCtrlD` in `tui/src/confirm_ctrl_d.rs` covering disabled mode, confirmation press, and timeout expiration.
|
||||
|
||||
**How it works**
|
||||
- When `require_double_ctrl_d = true`, the first Ctrl+D press shows "Press Ctrl+D again to confirm exit" in the status overlay.
|
||||
- A second Ctrl+D within `double_ctrl_d_timeout_secs` exits the TUI; otherwise the prompt and state clear after timeout.
|
||||
- When `require_double_ctrl_d = false`, Ctrl+D exits immediately as before.
|
||||
- Child processes still receive SIGINT normally since only the TUI event loop intercepts Ctrl+D.
|
||||
|
||||
## Notes
|
||||
|
||||
- Make the double‑Ctrl+D timeout duration configurable if desired (e.g. via `tui.double_ctrl_d_timeout_secs`).
|
||||
- Ensure that existing tests for Ctrl+D behavior are updated or new tests added to cover the confirmation state.
|
||||
@@ -0,0 +1,46 @@
|
||||
+++
|
||||
id = "18"
|
||||
title = "Chat UI Textarea Overlay and Border Styling Fix"
|
||||
status = "Merged"
|
||||
dependencies = "02,07,09,11,14,29"
|
||||
last_updated = "2025-06-25T05:36:27.942304"
|
||||
+++
|
||||
|
||||
# Task 18: Chat UI Textarea Overlay and Border Styling Fix
|
||||
|
||||
---
|
||||
id: 18
|
||||
title: Chat UI Textarea Overlay and Border Styling Fix
|
||||
status: Not started
|
||||
summary: Fix overlay of waiting messages and streamline borders between chat window and input area to improve visibility and reclaim terminal space.
|
||||
goal: |
|
||||
Adjust the TUI chat interface so that waiting/status messages no longer overlay the first line of the input textarea (ensuring user drafts remain visible), and merge/remove borders as follows:
|
||||
- Merge the bottom border of the chat history window with the top border of the input textarea.
|
||||
- Remove the left, right, and bottom overall borders around the chat interface to reduce wasted space.
|
||||
---
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Waiting/status messages (e.g. "Thinking...", "Typing...", etc.) appear above the textarea rather than overlaying the first line of the input area.
|
||||
- User draft text remains visible at all times, even when agent messages or status indicators are rendered.
|
||||
- The bottom border of the chat history pane and the top border of the textarea are unified into a single border line.
|
||||
- The left, right, and bottom borders around the entire chat UI are removed, reclaiming columns/rows in the terminal.
|
||||
- Manual or automated visual verification steps demonstrate correct layout in a variety of terminal widths.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
* Merged the bottom border of the history pane and the top border of the input textarea into a single shared line by removing the textarea's top border and keeping only a bottom border on the textarea and both top/bottom borders on the history pane.*
|
||||
* Removed left/right borders on both panes (history and textarea) and removed the textarea's bottom border from the overall UI to reclaim horizontal space.*
|
||||
* Updated the status-indicator overlay to render in its own floating box immediately above the textarea instead of covering the first input line.*
|
||||
|
||||
**How it works**
|
||||
At runtime the conversation history widget now draws only its top and bottom borders. The input textarea draws only its bottom border, carrying the help title there. These changes yield a single continuous border line separating history from input and eliminate the outer left, right, and bottom borders. Status messages ("Thinking...", etc.) render in a separate floating box positioned just above the textarea, leaving the user's draft text visible at all times.
|
||||
|
||||
## Notes
|
||||
|
||||
- This involves updating the rendering logic in the TUI modules (likely under `tui/src/` in `codex-rs`).
|
||||
- Ensure layout changes do not break existing tests or rendering in unusual terminal sizes.
|
||||
- Consider writing a simple snapshot test or manual demo script to validate border and overlay behavior.
|
||||
@@ -0,0 +1,42 @@
|
||||
+++
|
||||
id = "19"
|
||||
title = "Bash Command Rendering Improvements for Less Verbosity"
|
||||
status = "Merged"
|
||||
dependencies = "02,07,09,11,14,29"
|
||||
last_updated = "2025-06-25T05:36:32.641375"
|
||||
+++
|
||||
|
||||
> *This task is specific to per-agent UI conventions and log readability.*
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Shell commands render as plain text without `bash -lc` wrappers.
|
||||
- Role labels and message content appear on the same line, separated by a space.
|
||||
- Command-result annotations show a checkmark and duration for zero exit codes, or `exit code: N` and duration for nonzero codes, in the format `<icon or exit code> <duration>ms`.
|
||||
- Existing functionality remains unaffected beyond formatting changes.
|
||||
- Verbose background event logs (e.g. sandbox‑denied exec errors, retries) collapse into a single command execution entry showing command start, running indicator, and concise completion status.
|
||||
- Automated examples or tests verify the new rendering behavior.
|
||||
|
||||
## Implementation
|
||||
This change will touch both the event-processing and rendering layers of the Rust TUI:
|
||||
|
||||
- **Event processing** (`codex-rs/exec/src/event_processor.rs`):
|
||||
- Strip any `bash -lc` wrapper when formatting shell commands via `escape_command`.
|
||||
- Replace verbose `BackgroundEvent` logs for sandbox-denied errors and automatic retries with a unified exec-command begin/end sequence.
|
||||
- Annotate completed commands with either a checkmark (✅) and `<duration>ms` for success or `exit code: N <duration>ms` for failures.
|
||||
|
||||
- **TUI rendering** (`codex-rs/tui/src/history_cell.rs`):
|
||||
- Collapse consecutive `BackgroundEvent` entries related to exec failures/retries into the standard active/completed exec-command cells.
|
||||
- Update `new_active_exec_command` and `new_completed_exec_command` to use the new inline format (icon or exit code + duration, with `$ <command>` on the same block).
|
||||
- Ensure role labels and plain-text messages render on a single line separated by a space.
|
||||
|
||||
- **Tests** (`codex-rs/tui/tests/`):
|
||||
- Add or update test fixtures to verify:
|
||||
- Commands appear without any `bash -lc` boilerplate.
|
||||
- Completed commands show the correct checkmark or exit-code annotation with accurate duration formatting.
|
||||
- Background debugging events no longer leak raw debug strings and are correctly collapsed into the exec-command flow.
|
||||
|
||||
## Notes
|
||||
|
||||
- Improves readability of interactive sessions and logs by reducing boilerplate.
|
||||
- Ensure compatibility with both live TUI output and persisted log transcripts.
|
||||
34
agentydragon/tasks/.done/21-compact-markdown-rendering.md
Normal file
34
agentydragon/tasks/.done/21-compact-markdown-rendering.md
Normal file
@@ -0,0 +1,34 @@
|
||||
+++
|
||||
id = "21"
|
||||
title = "Compact Markdown Rendering Option"
|
||||
status = "Merged"
|
||||
dependencies = "03,06,08,13,15,32,18,19,22,23"
|
||||
last_updated = "2025-06-25T05:55:23.855039"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
Provide an option to render Markdown without blank lines between headings and content for more vertical packing.
|
||||
|
||||
## Goal
|
||||
Add a configuration flag to control Markdown rendering in the chat UI and logs so that headings render immediately adjacent to their content with no separating blank line.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Introduce a config flag `markdown_compact = true|false` under the UI settings.
|
||||
- When enabled, the renderer omits the default blank line between headings (lines starting with `#`) and their subsequent content.
|
||||
- The flag applies globally to all Markdown rendering (diffs, docs, help messages).
|
||||
- Default behavior remains unchanged (blank lines preserved) when `markdown_compact` is false or unset.
|
||||
- Add tests to verify both compact and default rendering modes across heading levels.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Extend the Markdown-to-TUI formatter to check `markdown_compact` and collapse heading/content spacing.
|
||||
- Implement a post-processing step that removes blank lines immediately following heading tokens (`^#{1,6} `) when `markdown_compact` is true.
|
||||
- Expose the new flag via the config parser and default it to `false`.
|
||||
- Add unit tests covering H1–H6 headings, verifying absence of blank line in compact mode and presence in default mode.
|
||||
|
||||
## Notes
|
||||
|
||||
- This option improves vertical density for screens with limited height.
|
||||
- Ensure compatibility with existing Markdown features like lists and code blocks; only target heading-content spacing.
|
||||
@@ -0,0 +1,41 @@
|
||||
+++
|
||||
id = "23"
|
||||
title = "Interactive Container Command Affordance via Hotkey"
|
||||
status = "Merged"
|
||||
freeform_status = ""
|
||||
dependencies = "01"
|
||||
last_updated = "2025-06-25T12:10:10.584536"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
Provide a keybinding to run arbitrary shell commands in the agent’s container and display output inline.
|
||||
|
||||
## Goal
|
||||
Add a user-facing affordance (e.g. a hotkey) to invoke arbitrary shell commands within the agent's container during a session for on-demand inspection and debugging. The typed command should be captured as a chat turn, executed via the existing shell tool, and its output rendered inline in the chat UI.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Bind a hotkey (e.g. Ctrl+M) that opens a prompt for the user to type any shell command.
|
||||
- When the user submits, capture the command as if entered in the chat input, and invoke the shell tool with the command in the agent’s container.
|
||||
- Display the command invocation and its stdout/stderr output inline in the chat window, respecting formatting rules (e.g. compact rendering settings).
|
||||
- Support chaining multiple commands in separate turns; history should show these command turns normally.
|
||||
- Provide unit or integration tests simulating a user hotkey press, command input, and verifying the shell tool is called and output is displayed.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Added a new slash command `Shell` and updated dispatch logic in `app.rs` to push a shell-command view.
|
||||
- Bound `Ctrl+M` in `ChatComposer` to dispatch `SlashCommand::Shell` for hotkey-driven shell prompt.
|
||||
- Created `ShellCommandView` (bottom pane overlay) to capture arbitrary user input and emit `AppEvent::ShellCommand(cmd)`.
|
||||
- Extended `AppEvent` with `ShellCommand(String)` and `ShellCommandResult { call_id, stdout, stderr, exit_code }` variants for round-trip messaging.
|
||||
- Implemented `ChatWidget::handle_shell_command` to execute `sh -c <cmd>` asynchronously (tokio::spawn) and send back `ShellCommandResult`.
|
||||
- Updated `ConversationHistoryWidget` to reuse existing exec-command cells to display shell commands and their output inline.
|
||||
- Added tests:
|
||||
- Unit test in `shell_command_view.rs` asserting correct event emission (skipping redraws).
|
||||
- Integration test in `chat_composer.rs` asserting `Ctrl+M` opens the shell prompt view and allows input.
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- This feature aids debugging and inspection without leaving the agent workflow.
|
||||
- Ensure that security policies (e.g. sandbox restrictions) still apply to these commands.
|
||||
@@ -0,0 +1,36 @@
|
||||
+++
|
||||
id = "28"
|
||||
title = "Include Command Snippet in Session-Scoped Approval Label"
|
||||
status = "Merged"
|
||||
dependencies = "03,06,08,13,15,32,18,19,22,23"
|
||||
last_updated = "2025-06-25T04:04:47.399379"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
When asking for session-scoped approval of a command, embed a truncated snippet of the actual command in the approval label for clarity.
|
||||
|
||||
## Goal
|
||||
Improve the session-scoped approval option label for commands by including a backtick-quoted snippet of the command itself (truncated to fit). This makes it clear exactly which command (including parameters) will be auto-approved for the session.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- The session-scoped approval label changes from generic text to include a snippet of the current command, e.g.:
|
||||
```text
|
||||
Yes, always allow running `cat x | foo --bar > out` for this session (a)
|
||||
```
|
||||
- If the command is too long, truncate the middle (e.g. `long-part…end-part`) to fit a configurable max length.
|
||||
- Implement the snippet templating in both Rust and JS UIs for consistency.
|
||||
- Add unit tests to verify snippet extraction, truncation logic, and label rendering for various command lengths.
|
||||
|
||||
## Implementation
|
||||
|
||||
**Planned implementation**
|
||||
- Add a `truncateMiddle` helper in both the Rust TUI and the JS/TS UI to ellipsize command snippets in the middle.
|
||||
- Extract the first line of the command string (up to any newline), truncate to a default max length (e.g. 30 characters), inserting a single-character ellipsis `…` when needed.
|
||||
- In the session-scoped approval option, replace the static label with a dynamic one:
|
||||
`Yes, always allow running `<snippet>` for this session (a)`.
|
||||
- Write unit tests for the helper and label generation covering commands shorter than, equal to, and longer than the max length.
|
||||
|
||||
## Notes
|
||||
|
||||
- This clarifies what parameters will be auto-approved and avoids ambiguity when multiple similar commands occur.
|
||||
@@ -0,0 +1,38 @@
|
||||
+++
|
||||
id = "31"
|
||||
title = "Display Remaining Context Percentage in codex-rs TUI"
|
||||
status = "Merged"
|
||||
dependencies = "03,06,08,13,15,32,18,19,22,23"
|
||||
last_updated = "2025-06-25T01:40:09.600000"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
Show a live "x% context left" indicator in the TUI (Rust) to inform users of remaining model context buffer.
|
||||
|
||||
## Goal
|
||||
Enhance the codex-rs TUI by adding a status indicator that displays the percentage of model context buffer remaining (e.g. "75% context left"). Update this indicator dynamically as the conversation progresses.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Compute current token usage and total context limit from the active session.
|
||||
- Display "<N>% context left" in the status bar or header of the TUI, formatted compactly.
|
||||
- Update the percentage after each message turn in real time.
|
||||
- Ensure the indicator is visible but does not obstruct existing UI elements.
|
||||
- Add unit or integration tests mocking token count updates and verifying correct percentage formatting (rounding behavior, boundary conditions).
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Added a `history_items: Vec<ResponseItem>` field to `ChatWidget` to accumulate the raw sequence of messages and function calls.
|
||||
- Created a new module `tui/src/context.rs` mirroring the JS heuristics:
|
||||
- `approximate_tokens_used(&[ResponseItem])`: counts characters in text and function-call items, divides by 4 and rounds up.
|
||||
- `max_tokens_for_model(&str)`: uses a registry of known model limits and heuristic fallbacks (32k, 16k, 8k, 4k, default 128k).
|
||||
- `calculate_context_percent_remaining(&[ResponseItem], &str)`: computes `(remaining / max) * 100`.
|
||||
- Updated `ChatWidget::replay_items` and `ChatWidget::handle_codex_event` to push each incoming `ResponseItem` into `history_items`.
|
||||
- Modified `ChatComposer::render_ref` to query `calculate_context_percent_remaining`, format and display "<N>% context left" after the input area, coloring it green/yellow/red per thresholds (>40%, 25–40%, ≤25%).
|
||||
- Added unit tests in `tui/tests/context_percent.rs` covering token counting, model heuristics, percent rounding, and boundary conditions.
|
||||
|
||||
## Notes
|
||||
|
||||
- This feature helps users anticipate when they may need to truncate history or start a new session.
|
||||
- Future enhancement: allow toggling this indicator on/off via config.
|
||||
42
agentydragon/tasks/.done/35-tui-inspect-env-integration.md
Normal file
42
agentydragon/tasks/.done/35-tui-inspect-env-integration.md
Normal file
@@ -0,0 +1,42 @@
|
||||
+++
|
||||
id = "35"
|
||||
title = "TUI Integration for Inspect-Env Command"
|
||||
status = "Done"
|
||||
dependencies = "10" # Rationale: depends on Task 10 for container state inspection
|
||||
last_updated = "2025-06-25T11:38:19Z"
|
||||
+++
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Done
|
||||
**Summary**: Follow-up to Task 10; add slash-command and TUI bindings for `inspect-env`.
|
||||
|
||||
## Goal
|
||||
|
||||
Add an `/inspect-env` slash-command in the TUI that invokes the existing `codex inspect-env` logic to display sandbox state inline.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Extend `SlashCommand` enum to include `InspectEnv`.
|
||||
- Dispatch `AppEvent::InlineInspectEnv` when `/inspect-env` is entered.
|
||||
- Handle `InlineInspectEnv` in `app.rs` to run `inspect-env` logic and stream its output to the TUI log pane.
|
||||
- Render mounts, permissions, and network status in a formatted table or tree view in the bottom pane.
|
||||
- Unit/integration tests simulating slash-command invocation and verifying rendered output.
|
||||
|
||||
## Implementation
|
||||
|
||||
**High-level approach**
|
||||
- Extend `SlashCommand` enum with `InspectEnv` and provide user-visible description.
|
||||
- Add `InlineInspectEnv` variant to `AppEvent` enum to represent inline slash-command invocation.
|
||||
- Update dispatch logic in `App::run` to spawn a background thread on `InlineInspectEnv` that runs `codex inspect-env`, reads its stdout line-by-line, and sends each line as `AppEvent::LatestLog`, then triggers a redraw.
|
||||
- Wire up `/inspect-env` to dispatch `InlineInspectEnv` in the slash-command handling.
|
||||
- Add unit tests in the TUI crate to verify `built_in_slash_commands()` includes `inspect-env` mapping and description, and tests for the command-popup filter to ensure `InspectEnv` is listed when `/inspect-env` is entered.
|
||||
|
||||
**How it works**
|
||||
When the user enters `/inspect-env`, the TUI parser recognizes the command and emits `AppEvent::InlineInspectEnv`. The main event loop handles this event by spawning a thread that invokes the external `codex inspect-env` command, captures its output line-by-line, and forwards each line into the TUI log pane via `AppEvent::LatestLog`. A redraw is scheduled once the inspection completes.
|
||||
|
||||
## Notes
|
||||
|
||||
- Reuse formatting code from `cli/src/inspect_env.rs` for consistency.
|
||||
@@ -0,0 +1,34 @@
|
||||
+++
|
||||
id = "38"
|
||||
title = "Fix Approval Dialog Transparent Background"
|
||||
status = "Done"
|
||||
dependencies = ""
|
||||
summary = "The approval dialog background is transparent, causing prompt text underneath to overlap and become unreadable."
|
||||
last_updated = "2025-06-25T23:00:00.000000"
|
||||
+++
|
||||
|
||||
> *UI bug:* When the approval dialog appears, its background is transparent and any partially entered prompt text shows through, overlapping and confusing the dialog.
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Done
|
||||
**Summary**: Identify and implement an opaque background for the approval dialog to prevent underlying text bleed-through.
|
||||
|
||||
## Goal
|
||||
|
||||
Ensure the approval dialog is drawn with a solid background color (matching the dialog border or theming) so that any underlying text does not bleed through.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Approval dialogs block underlying prompt text (solid background).
|
||||
- Existing unit/integration tests validate dialog visual rendering.
|
||||
|
||||
## Implementation
|
||||
|
||||
- Updated `render_ref` in `codex-rs/tui/src/user_approval_widget.rs` to fill the entire dialog area with a `DarkGray` background before drawing the border and content.
|
||||
- Implemented nested loops over the dialog `Rect` calling `buf[(col, row)].set_bg(Color::DarkGray)` on each cell.
|
||||
- Added unit test `render_approval_dialog_fills_background` in `tui/src/user_approval_widget.rs` to render the widget onto a buffer pre-filled with a red background and verify no cell in the dialog region remains transparent or retains the sentinel background.
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Any implementation notes -->
|
||||
47
agentydragon/tasks/02-auto-approve-predicates.md
Normal file
47
agentydragon/tasks/02-auto-approve-predicates.md
Normal file
@@ -0,0 +1,47 @@
|
||||
+++
|
||||
id = "02"
|
||||
title = "Granular Auto-Approval Predicates"
|
||||
status = "Done"
|
||||
dependencies = "11" # Rationale: depends on Task 11 for user-configurable approval predicates
|
||||
last_updated = "2025-06-25T10:48:30.000000"
|
||||
+++
|
||||
|
||||
# Task 02: Granular Auto-Approval Predicates
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
**General Status**: Done
|
||||
**Summary**: Added granular auto-approval predicates: configuration parsing, predicate evaluation, integration, documentation, and tests.
|
||||
|
||||
## Goal
|
||||
Let users configure one or more scripts in `config.toml` that examine each proposed shell command and return exactly one of:
|
||||
|
||||
- `deny` => auto-reject (skip sandbox and do not run the command)
|
||||
- `allow` => auto-approve and proceed under the sandbox
|
||||
- `no-opinion` => no opinion (neither approve nor reject)
|
||||
|
||||
Multiple scripts cast votes: if any script returns `deny`, the command is denied; otherwise if any script returns `allow`, the command is allowed; otherwise (all scripts return `no-opinion` or exit non-zero), pause for manual approval (existing logic).
|
||||
|
||||
## Acceptance Criteria
|
||||
- New `[[auto_allow]]` table in `config.toml` supporting one or more `script = "..."` entries.
|
||||
- Before running any shell/subprocess, Codex invokes each configured script in order, passing the candidate command as an argument.
|
||||
- If a script returns `deny` or `allow`, immediately take that vote and skip remaining scripts.
|
||||
- After all scripts complete with only `no-opinion` results or errors, pause for manual approval (existing logic).
|
||||
|
||||
- Spawn each predicate script with the full command as its only argument.
|
||||
- Parse stdout (case-insensitive) expecting `deny`, `allow`, or `no-opinion`, treating errors or unknown output as `NoOpinion`.
|
||||
- Short-circuit on the first `Deny` or `Allow` vote.
|
||||
- A `Deny` vote aborts execution.
|
||||
- An `Allow` vote skips prompting and proceeds under sandbox.
|
||||
- All `NoOpinion` votes fall back to existing approval logic.
|
||||
|
||||
## Implementation
|
||||
-- Added `auto_allow: Vec<AutoAllowPredicate>` to `ConfigToml`, `ConfigProfile`, and `Config` to parse `[[auto_allow]]` entries from `config.toml`.
|
||||
-- Defined `AutoAllowPredicate { script: String }` and `AutoAllowVote { Allow, Deny, NoOpinion }` in `core::safety`.
|
||||
-- Implemented `evaluate_auto_allow_predicates` in `core::safety` to spawn each script with the candidate command, parse its stdout vote, and short-circuit on `Deny` or `Allow`.
|
||||
-- Integrated `evaluate_auto_allow_predicates` into the shell execution path in `core::codex`, aborting on `Deny`, auto-approving on `Allow`, and falling back to manual or policy-based approval on `NoOpinion`.
|
||||
-- Updated `config.md` to document the `[[auto_allow]]` table syntax and behavior.
|
||||
-- Added comprehensive unit tests covering vote parsing, error propagation, short-circuit behavior, and end-to-end predicate functionality.
|
||||
## Notes
|
||||
- This pairs with the existing `approval_policy = "unless-allow-listed"` but adds custom logic before prompting.
|
||||
63
agentydragon/tasks/04-auto-mount-repo.md
Normal file
63
agentydragon/tasks/04-auto-mount-repo.md
Normal file
@@ -0,0 +1,63 @@
|
||||
+++
|
||||
id = "04"
|
||||
title = "Auto-Mount Entire Repo and Auto-CD to Subfolder"
|
||||
status = "Not started"
|
||||
dependencies = "01" # Rationale: depends on Task 01 for mount-add/remove foundational commands
|
||||
last_updated = "2025-06-25T01:40:09.800000"
|
||||
+++
|
||||
|
||||
# Task 04: Auto-Mount Entire Repo and Auto-CD to Subfolder
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Subtasks
|
||||
|
||||
Subtasks to implement in order all in one P:
|
||||
|
||||
### 04.1 – Config → `ConfigToml` + `Config`
|
||||
- Add `auto_mount_repo: bool` and `mount_prefix: String` to `ConfigToml` (with proper `#[serde(default)]` and defaults).
|
||||
- Wire these fields through to the `Config` struct.
|
||||
|
||||
### 04.2 – Git root detection + relative‐path
|
||||
- Implement a helper in `codex_core::util` to locate the Git repository root given a starting `cwd`.
|
||||
- Compute the sub‐directory path relative to the repo root.
|
||||
|
||||
### 04.3 – Bind‑mount logic
|
||||
- In the sandbox startup path (`apply_sandbox_policy_to_current_thread` or a new wrapper before it), if `auto_mount_repo` is set:
|
||||
- Bind‑mount `repo_root` → `mount_prefix` (e.g. `/workspace`).
|
||||
- Create target directory if missing.
|
||||
|
||||
### 04.4 – Automate `cwd` → new mount
|
||||
- After mounting, update the process‐wide `cwd` to `mount_prefix/relative_path` so all subsequent file ops occur under the mount.
|
||||
|
||||
### 04.5 – Config docs & tests
|
||||
- Update `config.md` to document `auto_mount_repo` and `mount_prefix` under the top‐level config.
|
||||
- Add unit tests for the Git‐root helper and default values.
|
||||
|
||||
### 04.6 – E2E manual verification
|
||||
- Manually verify launching with `auto_mount_repo = true` in a nested subfolder:
|
||||
- TTY prompt shows sandboxed cwd under `/workspace/<subdir>`.
|
||||
- Commands executed by Codex see the mount.
|
||||
|
||||
## Goal
|
||||
Allow users to enable a flag so that each session:
|
||||
|
||||
1. Detects the Git repository root of the current working directory.
|
||||
2. Bind-mounts the entire repository into `/workspace` in the session.
|
||||
3. Changes directory to `/workspace/<relative-path-from-root>` to mirror the user’s original subfolder.
|
||||
|
||||
## Acceptance Criteria
|
||||
- New `auto_mount_repo = true` and optional `mount_prefix = "/workspace"` in `config.toml`.
|
||||
- Before any worktree or mount processing, detect the Git root, bind-mount it to `mount_prefix`, and set `cwd` to `mount_prefix + relative_path`.
|
||||
- Existing worktree/session-worktree logic should operate relative to this new `cwd`.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
*(Not implemented yet)*
|
||||
|
||||
**How it works**
|
||||
*(Not implemented yet)*
|
||||
|
||||
## Notes
|
||||
- This offloads the entire monorepo into the session, leaving the user’s original clone untouched.
|
||||
47
agentydragon/tasks/09-file-dir-level-approvals.md
Normal file
47
agentydragon/tasks/09-file-dir-level-approvals.md
Normal file
@@ -0,0 +1,47 @@
|
||||
+++
|
||||
id = "09"
|
||||
title = "File- and Directory-Level Approvals"
|
||||
status = "Not started"
|
||||
dependencies = "11" # Rationale: depends on Task 11 for custom approval predicate infrastructure
|
||||
last_updated = "2025-06-25T01:40:09.507043"
|
||||
+++
|
||||
|
||||
# Task 09: File- and Directory-Level Approvals
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Not started
|
||||
**Summary**: Not started; missing Implementation details (How it was implemented and How it works).
|
||||
|
||||
## Goal
|
||||
|
||||
Enable fine-grained approval controls so users can whitelist edits scoped to specific files or directories at runtime, with optional time limits.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- In the approval dialog, offer “Allow this file always” and “Allow this directory always” options alongside proceed/deny.
|
||||
- Prompt for a time limit when granting a file/dir approval, with default presets (e.g. 5 min, 1 hr, 4 hr, 24 hr).
|
||||
- Introduce runtime commands to inspect and manage granular approvals:
|
||||
- `/approvals list` to view active approvals and remaining time
|
||||
- `/approvals add [file|dir] <path> [--duration <preset>]` to grant approval
|
||||
- `/approvals remove <id>` to revoke an approval
|
||||
- Persist granular approvals in session metadata, keyed by working directory. On session resume in a different directory, warn the user and discard all file/dir approvals.
|
||||
- Automatically expire and remove approvals when their time limits elapse.
|
||||
- Reflect file/dir-approval state in the CLI shell prompt or title for quick visibility.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
*(Not implemented yet)*
|
||||
|
||||
**How it works**
|
||||
*(Not implemented yet)*
|
||||
|
||||
## Notes
|
||||
|
||||
- Store approvals with {id, scope: file|dir, path, expires_at} in session JSON.
|
||||
- Use a background timer or check-before-command to prune expired entries.
|
||||
- Reuse existing command-parsing infrastructure to implement `/approvals` subcommands.
|
||||
- Consider UI/UX for selecting presets in TUI dialogs.
|
||||
44
agentydragon/tasks/12-internet-connection-toggle.md
Normal file
44
agentydragon/tasks/12-internet-connection-toggle.md
Normal file
@@ -0,0 +1,44 @@
|
||||
+++
|
||||
id = "12"
|
||||
title = "Runtime Internet Connection Toggle"
|
||||
status = "Not started"
|
||||
dependencies = "" # No prerequisites
|
||||
last_updated = "2025-06-25T01:40:09.509507"
|
||||
+++
|
||||
|
||||
# Task 12: Runtime Internet Connection Toggle
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Not started
|
||||
**Summary**: Not started; missing Implementation details (How it was implemented and How it works).
|
||||
|
||||
## Goal
|
||||
|
||||
Allow users to enable or disable internet access at runtime within their container/sandbox session.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Slash command or CLI subcommand (`/toggle-network <on|off>`) to turn internet on or off immediately.
|
||||
- Persist network state in session metadata so that resuming a session restores the last setting.
|
||||
- Enforce the new network policy dynamically: block or allow outbound network connections without restarting the agent.
|
||||
- Reflect the current network status in the CLI prompt or shell title (e.g. 🌐/🚫).
|
||||
- Work across supported platforms (Linux sandbox, macOS Seatbelt, Windows) using appropriate sandbox APIs.
|
||||
- Include unit and integration tests to verify network toggle behavior and persistence.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
*(Not implemented yet)*
|
||||
|
||||
**How it works**
|
||||
*(Not implemented yet)*
|
||||
|
||||
## Notes
|
||||
|
||||
- Reuse the existing sandbox network-disable mechanism (`CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR`) for toggling.
|
||||
- On Linux, this may involve updating Landlock or seccomp rules at runtime.
|
||||
- On macOS, interact with the Seatbelt profile; consider session restart if necessary.
|
||||
- When persisting state, store a `network_enabled: bool` flag in the session JSON.
|
||||
47
agentydragon/tasks/14-ai-generated-approval-predicates.md
Normal file
47
agentydragon/tasks/14-ai-generated-approval-predicates.md
Normal file
@@ -0,0 +1,47 @@
|
||||
+++
|
||||
id = "14"
|
||||
title = "AI‑Generated Approval Predicate Suggestions"
|
||||
status = "Not started"
|
||||
dependencies = "02,11" # Rationale: depends on Task 02 for auto-approval predicates and Task 11 for predicate invocation logic
|
||||
last_updated = "2025-06-25T01:40:09.511783"
|
||||
+++
|
||||
|
||||
# Task 14: AI‑Generated Approval Predicate Suggestions
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Not started
|
||||
**Summary**: Not started; missing Implementation details (How it was implemented and How it works).
|
||||
|
||||
## Goal
|
||||
|
||||
When a shell command is not auto-approved, the approval prompt should include 1–3 AI-generated approval predicates. Each suggestion is a time-limited Python predicate snippet plus an explanation of the full set of permissions it would grant. Users can pick one suggestion to append to the session’s approval policy as a broader-scope allow rule.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- When a command is not auto-approved, show up to 3 suggested predicates inline in the TUI approval dialog.
|
||||
- Each suggestion consists of:
|
||||
- A Python code snippet defining a predicate function.
|
||||
- An AI-generated explanation of exactly what permissions or scope that predicate grants.
|
||||
- A TTL or expiration timestamp indicating how long it will remain active.
|
||||
- Users can select one suggestion to append to the session’s list of approval predicates.
|
||||
- Predicates are stored in session state (in-memory) for the duration of the session.
|
||||
- Provide a slash/CLI command (`/inspect-approval-predicates`) to list current predicates, their code, explanations, and timeouts.
|
||||
- Support headless and interactive modes equally.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
*(Not implemented yet)*
|
||||
|
||||
**How it works**
|
||||
*(Not implemented yet)*
|
||||
|
||||
## Notes
|
||||
|
||||
- Reuse the existing AI reasoning engine to generate predicate suggestions.
|
||||
- Represent predicates as Python functions returning a boolean.
|
||||
- Ensure that expiration is enforced and stale predicates are ignored.
|
||||
- Integrate the new `/inspect-approval-predicates` command into both the TUI and Exec CLI.
|
||||
28
agentydragon/tasks/17-sandbox-precommit-permission-error.md
Normal file
28
agentydragon/tasks/17-sandbox-precommit-permission-error.md
Normal file
@@ -0,0 +1,28 @@
|
||||
+++
|
||||
id = "17"
|
||||
title = "Sandbox Pre-commit Permission Error"
|
||||
status = "Not started"
|
||||
dependencies = "15" # Rationale: depends on Task 15 for sandbox worktree configuration
|
||||
last_updated = "2025-06-25T01:41:34.737190"
|
||||
+++
|
||||
|
||||
> *This task addresses scaffolding/setup for Agent worktrees.*
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Pre-commit hooks detect sandbox environment and skip or override gitconfig locking.
|
||||
- Documentation in scaffold guides is updated to note pre-commit limitations and workarounds.
|
||||
- Verification steps demonstrate pre-commit hooks succeeding in sandbox without modifying user gitconfig.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
*(Not implemented yet)*
|
||||
|
||||
**How it works**
|
||||
*(Not implemented yet)*
|
||||
|
||||
## Notes
|
||||
|
||||
- The sandbox prevents locking ~/.gitconfig, leading to PermissionError.
|
||||
- Consider configuring pre-commit to use a repo-local config or skip locking by passing `--config` or setting `PRE_COMMIT_HOME`.
|
||||
@@ -0,0 +1,36 @@
|
||||
+++
|
||||
id = "20"
|
||||
title = "Render Patch Content in Chat Display Window for Approve/Deny"
|
||||
status = "Not started"
|
||||
dependencies = "" # No prerequisites
|
||||
last_updated = "2025-06-25T01:41:34.738344"
|
||||
+++
|
||||
|
||||
> *This task is specific to the chat UI renderer.*
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- When displaying a patch for approve/deny, the full diff for the active patch is rendered inline in the chat window.
|
||||
- Older or superseded patches collapse to show only up to N lines of context, with an indicator (e.g. "... 10 lines collapsed ...").
|
||||
- File paths in diff headers are shown relative to the current working directory, unless the file resides outside the CWD.
|
||||
- Event logs around patch application are simplified: drop structured event data and replace with a simple status note (e.g. "patch applied").
|
||||
- Configurable parameter (e.g. `patch_context_lines`) controls the number of context lines for collapsed hunks.
|
||||
- Preserve the user’s draft input when an approval dialog or patch diff appears; ensure the draft editor remains visible so users can continue editing while reviewing.
|
||||
- Provide end-to-end integration tests that simulate drafting long messages, triggering approval dialogs and overlays, and verify that all UI elements (draft editor, diffs, logs) render correctly without overlap or content loss.
|
||||
- Exhaustively test all dialog interaction flows (approve, deny, cancel) and overlay scenarios to confirm consistent behavior across combinations and prevent rendering artifacts.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Extend the chat renderer to detect patch approval prompts and render diffs using a custom formatter.
|
||||
- Compute relative paths via `Path::strip_prefix`, falling back to full path if outside CWD.
|
||||
- Track the current patch ID and render its full content; collapse previous patch bodies according to `patch_context_lines` setting.
|
||||
- Preserve and render the current draft buffer alongside the active patch diff, ensuring live edits remain visible during approval steps.
|
||||
- Add integration tests using the TUI test harness or end-to-end framework to simulate user input of long text, approval flows, overlay dialogs, and log output, asserting correct screen layout and content integrity.
|
||||
- Design a parameterized test matrix covering all dialog interaction flows (approve/deny/cancel) and overlay transitions to ensure exhaustive coverage and UI sanity.
|
||||
- Replace verbose event debug output with a single-line status message.
|
||||
|
||||
## Notes
|
||||
|
||||
- Users can override `patch_context_lines` in their config to see more or fewer collapsed lines.
|
||||
- Ensure compatibility with both live TUI sessions and persisted transcript logs.
|
||||
37
agentydragon/tasks/22-message-rendering-layout-options.md
Normal file
37
agentydragon/tasks/22-message-rendering-layout-options.md
Normal file
@@ -0,0 +1,37 @@
|
||||
+++
|
||||
id = "22"
|
||||
title = "Message Separation and Sender-Content Layout Options"
|
||||
status = "Done"
|
||||
dependencies = "" # No prerequisites
|
||||
last_updated = "2025-06-25T11:05:55.000000"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
Add configurable options for inter-message spacing and sender-content line breaks in chat rendering
|
||||
**in the codex-rs package** - **NOT** the codex-cli package.
|
||||
|
||||
## Goal
|
||||
Provide users with flexibility in how chat messages are visually separated and how sender labels are displayed relative to message content:
|
||||
- Control whether an empty line is inserted between consecutive messages.
|
||||
- Control whether sender and content appear on the same line or on separate lines.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Introduce one new config flags under the UI section:
|
||||
- `message_spacing: true|false` controls inserting a blank line between messages when true.
|
||||
- default to `false` to preserve current compact layout.
|
||||
- When `message_spacing` is enabled, render an empty line between each message bubble or block.
|
||||
- Add unit tests to verify the layout produces the correct sequence of lines.
|
||||
|
||||
## Implementation
|
||||
### Plan
|
||||
|
||||
**How it was implemented**
|
||||
- Extend the chat UI renderer to read `message_spacing` from config.
|
||||
- In the message rendering routine, after emitting each message block, conditionally insert a blank line if `message_spacing` is true.
|
||||
- Write unit tests for values of `(message_spacing)` covering single-line messages, multi-line content, and boundaries.
|
||||
|
||||
## Notes
|
||||
|
||||
- These options improve readability for users who prefer more visual separation or clearer sender labels.
|
||||
- Keep default settings unchanged to avoid surprising existing users.
|
||||
33
agentydragon/tasks/24-guard-tool-output-sequencing-js.md
Normal file
33
agentydragon/tasks/24-guard-tool-output-sequencing-js.md
Normal file
@@ -0,0 +1,33 @@
|
||||
+++
|
||||
id = "24"
|
||||
title = "Guard Against Missing Tool Output in JS Server Sequencing"
|
||||
status = "Not started"
|
||||
dependencies = "" # No prerequisites
|
||||
last_updated = "2025-06-25T01:40:09.600000"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
Prevent out-of-order chat messages and missing tool outputs when user input interrupts tool execution in the JS backend.
|
||||
|
||||
## Goal
|
||||
Ensure the JS server never emits a user or model message before the corresponding tool output has been delivered. Add sequencing guards to the message dispatcher so that aborted rollouts or interleaved user messages cannot cause "No tool output found" errors.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- When a tool invocation is interrupted or user sends a message mid-rollout, the JS server buffers subsequent messages until the tool output event arrives or the invocation is explicitly cancelled.
|
||||
- The server must never log or emit an error like "No tool output found for local shell call" due to sequencing mismatch.
|
||||
- Add automated tests simulating mid-rollout user interrupts in the JS test suite, verifying correct buffering and eventual message delivery or cancellation.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- In the JS message dispatcher, track pending tool invocations by ID and delay processing of new chat messages until the pending invocation resolves (success, failure, or cancel).
|
||||
- Add a guard in the `handleUserMessage` path to check for unresolved tool IDs before appending user content; if pending, queue the message.
|
||||
- On receiving `toolOutput` or `toolError` for an invocation ID, flush any queued messages in order.
|
||||
- Implement explicit cancellation paths so that if a tool invocation is abandoned, queued messages still flow after cancellation confirmation.
|
||||
- Add unit and integration tests in the JS test harness to cover normal, aborted, and concurrent message scenarios.
|
||||
|
||||
## Notes
|
||||
|
||||
- This change prevents 400 Bad Request errors from tool retries where the model requests a tool before the output is streamed.
|
||||
- Keep diagnostic logs around sequencing logic for troubleshooting but avoid spamming on normal race cases.
|
||||
78
agentydragon/tasks/25-guard-tool-output-sequencing-rust.md
Normal file
78
agentydragon/tasks/25-guard-tool-output-sequencing-rust.md
Normal file
@@ -0,0 +1,78 @@
|
||||
+++
|
||||
id = "25"
|
||||
title = "Guard Against Missing Tool Output in Rust Server Sequencing"
|
||||
status = "Needs input"
|
||||
dependencies = "" # No prerequisites
|
||||
last_updated = "2025-06-25T22:50:01.000000"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
Prevent out-of-order chat messages and missing tool output errors when user input interrupts tool execution in the Rust backend.
|
||||
|
||||
## Goal
|
||||
Ensure the Rust server implementation sequences tool output and chat messages correctly. Add synchronization logic so that an in-flight tool invocation either completes or is cancelled before new messages are processed, avoiding "No tool output found" invalid_request errors.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- The Rust message broker must detect pending tool invocations and pause delivery of subsequent user or model messages until the tool result or cancellation is handled.
|
||||
- No panic or 400 Bad Request errors should occur due to missing tool output in edge cases of interrupted rollouts or mid-stream user input.
|
||||
- Add Rust integration tests simulating tool invocation interruption and user message interleaving, verifying correct ordering and delivery.
|
||||
|
||||
## Implementation
|
||||
|
||||
We will implement the following high-level plan:
|
||||
|
||||
- Locate where the ChatCompletion request messages array is built in Rust:
|
||||
the `stream_chat_completions` function in `codex-rs/core/src/chat_completions.rs`.
|
||||
- In that loop, track pending tool invocations by their call IDs when encountering `ResponseItem::FunctionCall` entries.
|
||||
- Buffer any subsequent `ResponseItem::Message { role: "user" }` or new turn inputs until the matching `ResponseItem::FunctionCallOutput` (tool result) appears.
|
||||
- Once the tool output is seen, flush buffered user messages in order immediately before continuing to build the next API call.
|
||||
- Add tests under `codex-rs/core/tests/` (e.g. `guard_tool_output_sequencing.rs`) that exercise interleaved input sequences:
|
||||
- A user message mid-rollout before tool output, ensuring it is delayed until after the tool result.
|
||||
- Normal flow where no buffering is needed.
|
||||
- Cancellation paths (no tool output) inserting a fake "Tool cancelled" tool message and flushing buffered messages.
|
||||
- Front-end layers automatically render the "Tool cancelled" message as a tool output, satisfying UI requirements.
|
||||
|
||||
## Notes
|
||||
|
||||
```text
|
||||
█
|
||||
thinking █Deciding on file management █
|
||||
───╮
|
||||
I’m thinking that since file editing can be a hassle, it might be simpler to just remove the file. I could then ask the user to provide │any errors they’re facing. To do this, I'd create a patch to delete the specified file. Alternatively, I could consider reverting the │
|
||||
changes with git, but applying the patch seems like a straightforward solution. So, I’ll go ahead and prepare to apply the patch to ──╯
|
||||
delete the file.
|
||||
|
||||
proposed patch
|
||||
D /Users/mpokorny/code/codex/agentydragon/tasks/.worktrees/23-interactive-container-command-affordance/codex-rs/.cargo/config.toml ny
|
||||
ERROR: Turn interrupted
|
||||
|
||||
user
|
||||
why did you try to delete that? s P
|
||||
event
|
||||
BackgroundEvent(BackgroundEventEvent { message: "stream error: unexpected status 400 Bad Request: {\n \"error\": {\n \"message\": \"No tool output found for local shell call call_PjdhYGAM7q52HL13WF8slGn9.\",\n \"type\": \"invalid_request_error\",\n \"param\":
|
||||
\"input\",\n \"code\": null\n }\n}; retrying 1/10 in 201ms…" }) │
|
||||
│
|
||||
event │
|
||||
BackgroundEvent(BackgroundEventEvent { message: "stream error: unexpected status 400 Bad Request: {\n \"error\": {\n \"message\": │
|
||||
\"No tool output found for local shell call call_PjdhYGAM7q52HL13WF8slGn9.\",\n \"type\": \"invalid_request_error\",\n \"param\": │\"input\",\n \"code\": null\n }\n}; retrying 2/10 in 246ms…" }) │
|
||||
│
|
||||
event │BackgroundEvent(BackgroundEventEvent { message: "stream error: unexpected status 400 Bad Request: {\n \"error\": {\n \"message\": │
|
||||
\"No tool output found for local shell call call_PjdhYGAM7q52HL13WF8slGn9.\",\n \"type\": \"invalid_request_error\",\n \"param\": █
|
||||
\"input\",\n \"code\": null\n }\n}; retrying 3/10 in 371ms…" }) █
|
||||
|
||||
this is a lot of the problem still happening
|
||||
```
|
||||
|
||||
## Next Steps / Debugging
|
||||
|
||||
The above change did not resolve the issue. We need to gather more debug information to understand why missing tool output errors still occur.
|
||||
|
||||
Suggested approaches:
|
||||
- Enable detailed debug logging in the Rust message broker (e.g. set `RUST_LOG=debug` or add tracing spans around function calls).
|
||||
- Dump the sequence of incoming and outgoing `ResponseItem` events to a log file for offline analysis.
|
||||
- Instrument timing and ordering by recording timestamps when tool invocations start, complete, and when user input is received.
|
||||
- Write a minimal reproduction harness that reliably triggers the missing output error under controlled conditions.
|
||||
- Capture full request/response payloads to/from the OpenAI API to verify whether the function output is delivered but not processed.
|
||||
|
||||
Please expand this section with specific examples or helper scripts to collect the necessary data.
|
||||
36
agentydragon/tasks/26-separate-approval-dialog-from-draft.md
Normal file
36
agentydragon/tasks/26-separate-approval-dialog-from-draft.md
Normal file
@@ -0,0 +1,36 @@
|
||||
+++
|
||||
id = "26"
|
||||
title = "Render Approval Requests in Separate Dialog from Draft Window"
|
||||
status = "Not started"
|
||||
dependencies = "09,23" # Rationale: depends on Tasks 09 and 23 for file-level approvals and interactive command affordance
|
||||
last_updated = "2025-06-25T01:40:09.600000"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
Display patch approval prompts in a distinct dialog or panel to avoid overlaying the draft editor.
|
||||
|
||||
## Goal
|
||||
Change the chat UI so that approval requests (patch diffs for approve/deny) appear in a separate dialog element or panel, positioned adjacent to or below the chat window, rather than overlaying the draft input area.
|
||||
This eliminates overlay conflicts and ensures the draft editor remains fully visible and interactive while reviewing patches.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Approval prompts with diffs open in a distinct UI element (e.g. side panel or bottom pane) that does not obscure the draft editor.
|
||||
- The draft input area remains fully visible and editable whenever an approval dialog is active.
|
||||
- The approval dialog is visually distinguished (border, background) and clearly labeled.
|
||||
- The layout adjusts responsively for narrow/short terminal sizes, maintaining separation without clipping content.
|
||||
- Add functional tests or integration tests verifying that the draft input remains accessible and that the approval dialog contents are rendered in the new panel.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Refactor the patch-approval renderer to spawn a separate TUI view (`ApprovalDialogView`) instead of the overlay popup.
|
||||
- Allocate a consistent panel region (e.g. bottom X rows or right-hand column) for approval dialogs, reserving the draft editor region above or to the left.
|
||||
- Update layout logic to recalculate positions on terminal resize, ensuring both panels remain visible.
|
||||
- Style the new dialog with its own borders and title bar (e.g. "Approval Request").
|
||||
- Add integration tests using the TUI test harness to simulate opening approval prompts and verifying that typing in the draft area still works and that the dialog appears in the correct panel.
|
||||
|
||||
## Notes
|
||||
|
||||
- This change fixes the long-standing overlay bug where approval diffs obstruct the draft.
|
||||
- Future enhancements may allow toggling between inline overlay or separate panel modes.
|
||||
46
agentydragon/tasks/27-unified-sandbox-retry-prompt-rust.md
Normal file
46
agentydragon/tasks/27-unified-sandbox-retry-prompt-rust.md
Normal file
@@ -0,0 +1,46 @@
|
||||
+++
|
||||
id = "27"
|
||||
title = "Unified Sandbox-Retry Prompt with y/a/A/n Options (Rust)"
|
||||
status = "Not started"
|
||||
dependencies = "15,17" # Rationale: depends on Tasks 15 and 17 for sandbox configuration and pre-commit permission handling
|
||||
last_updated = "2025-06-25T01:40:09.600000"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
Implement a unified retry‑without‑sandbox prompt in the Rust TUI with one‑shot, session‑scoped, and persistent options.
|
||||
|
||||
## Goal
|
||||
Replace the two-stage sandbox‑retry and approval flow with a single, unified prompt in the Rust UI. Provide four hotkey options (y/a/A/n) to control sandbox behavior at varying scopes:
|
||||
- y: retry this one command without sandbox
|
||||
- a: always run without sandbox but still ask first
|
||||
- A: always run without sandbox and never ask again
|
||||
- n: keep using sandbox
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- When a sandboxed shell invocation fails (exit code ≠ 0), display a single prompt:
|
||||
```
|
||||
Retry without sandbox
|
||||
|
||||
y Yes, run without sandbox this one time
|
||||
a Yes, always run without sandbox but still ask me first
|
||||
A Yes, always run without sandbox and do not ask again
|
||||
n No, keep using sandbox
|
||||
```
|
||||
- Hotkeys y/a/A/n must map to the corresponding behavior and dismiss the prompt.
|
||||
- The prompt replaces the older two‑stage “retry?” + “Allow command?” dialogs.
|
||||
- Add unit/integration tests simulating a failing sandbox command and each hotkey path, verifying correct sandbox flag logic.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Refactor the sandbox error handler in `tui/src/shell.rs` to emit a single `SandboxRetryPrompt` event instead of separate prompts.
|
||||
- Create a new TUI widget `SandboxRetryWidget` that renders the four-line menu and captures y/a/A/n keys.
|
||||
- Map each choice to updating the per-session config (`Config.tui.sandbox_mode`) and retrying or aborting the command as appropriate.
|
||||
- Update the shell‑invocation pipeline to consult the new `sandbox_mode` setting and skip sandbox when indicated.
|
||||
- Write Rust tests (in `tui/tests/`) to simulate sandbox failures and user key presses for all four options.
|
||||
|
||||
## Notes
|
||||
|
||||
- This unifies and simplifies the UX, removing confusion from layered prompts.
|
||||
- The three levels of scope (one-off, scoped prompt, no prompt) give power users flexibility and safety.
|
||||
29
agentydragon/tasks/29-auto-approve-empty-tool-commands.md
Normal file
29
agentydragon/tasks/29-auto-approve-empty-tool-commands.md
Normal file
@@ -0,0 +1,29 @@
|
||||
+++
|
||||
id = "29"
|
||||
title = "Auto-Approve Empty-Array Tool Invocations"
|
||||
status = "Not started"
|
||||
dependencies = "02" # Rationale: depends on Task 02 for auto-approval logic
|
||||
last_updated = "2025-06-25T01:40:09.600000"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
Automatically approve tool-use requests where the command array is empty, bypassing the approval prompt.
|
||||
|
||||
## Goal
|
||||
In rare cases the model may emit a tool invocation event with an empty `command: []`. These invocations cannot succeed and continually trigger errors. Automatically treat empty-array tool requests as approved (once), suppressing the approval UI, to allow downstream error handling rather than perpetual prompts.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Detect tool requests where `command: []` (no arguments).
|
||||
- Do not open the approval prompt for these cases; instead, automatically approve and allow the tool pipeline to proceed (and eventually handle the error).
|
||||
- Include a unit test simulating an empty-array tool invocation that verifies no approval prompt is shown and that a `ReviewDecision::Approved` is returned immediately.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- In the command-review widget setup (`ApprovalRequest::Exec`), check for `command.is_empty()` before rendering; if empty, directly send `ReviewDecision::Approved` and mark the widget done.
|
||||
- Add a Rust unit test for `UserApprovalWidget` to feed an `Exec { command: vec![] }` request and assert automatic approval without rendering the select mode.
|
||||
|
||||
## Notes
|
||||
|
||||
- This is a pragmatic workaround for spurious empty‑command tool calls; a more robust model‑side fix may replace this later.
|
||||
41
agentydragon/tasks/30-non-fullscreen-scrollback-mode.md
Normal file
41
agentydragon/tasks/30-non-fullscreen-scrollback-mode.md
Normal file
@@ -0,0 +1,41 @@
|
||||
+++
|
||||
id = "30"
|
||||
title = "Non-Fullscreen Scrollback Mode with Native Terminal Scroll"
|
||||
status = "Not started"
|
||||
dependencies = "" # No prerequisites
|
||||
last_updated = "2025-06-25T01:40:09.600000"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
Offer a non-fullscreen TUI mode that appends conversation output and defers scrolling to the terminal scrollback.
|
||||
|
||||
## Goal
|
||||
Provide an optional non-fullscreen mode for the chat UI where:
|
||||
- The TUI does not capture the mouse scroll wheel.
|
||||
- All conversation output is appended in place, allowing the terminal's native scrollback to navigate history.
|
||||
- The user-entry window remains fixed at the bottom of the terminal.
|
||||
- The entire UI runs in a standard terminal buffer (no alternate screen), so the user can use their terminal’s scrollbar or scrollback keys to review past messages.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Introduce a `tui.non_fullscreen_mode` config flag (default `false`).
|
||||
- When enabled, the application:
|
||||
- Disables alternate screen buffering (i.e. does not switch to the TUI alt-screen).
|
||||
- Does not intercept mouse scroll events; scroll events are passed through to the terminal.
|
||||
- Renders new chat messages inline (appended) rather than redrawing the full viewport.
|
||||
- Keeps the user input prompt visible at the bottom after each message.
|
||||
- Add integration tests or manual validation steps to confirm that: scrollback keys/mouse scroll work via terminal scrollback, and the prompt remains in view.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Add `non_fullscreen_mode: bool` to the `tui` config section.
|
||||
- In the TUI initialization, skip entering the alternate screen and disable pannable viewports.
|
||||
- Remove mouse event capture for scroll wheel events when `non_fullscreen_mode` is true.
|
||||
- Change rendering loop: after each new message, print the message directly to the stdout buffer (in append mode), then redraw only the input prompt line.
|
||||
- Write integration tests that spawn the TUI in non-fullscreen mode, emit multiple messages, send scroll events (if possible), and assert that scrollback buffer contains the messages.
|
||||
|
||||
## Notes
|
||||
|
||||
- This mode trades advanced in-TUI scrolling features for simplicity and compatibility with users’ accustomed terminal scrollback.
|
||||
- It may not support complex viewport resizing; documentation should note that.
|
||||
49
agentydragon/tasks/32-embedded-neovim-prompt-editor.md
Normal file
49
agentydragon/tasks/32-embedded-neovim-prompt-editor.md
Normal file
@@ -0,0 +1,49 @@
|
||||
+++
|
||||
id = "32"
|
||||
title = "Embedded Neovim Prompt Editor"
|
||||
status = "Not started"
|
||||
dependencies = "06" # Rationale: depends on Task 06 for external editor integration
|
||||
last_updated = "2025-06-25T01:40:09.513224"
|
||||
+++
|
||||
|
||||
# Task 32: Embedded Neovim Prompt Editor
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Not started
|
||||
**Summary**: Not started; missing Implementation details (How it was implemented and How it works).
|
||||
|
||||
## Goal
|
||||
|
||||
Replace the basic line‑editing prompt composer with an embedded Neovim window so users can enjoy full-featured, multi-line editing of their chat prompt directly inside the TUI.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Introduce a TUI-integrated Neovim editor pane activated via `/edit-prompt` or `Ctrl+E` when `embedded_prompt_editor = true` in `[tui]` config.
|
||||
- Pre-populate the Neovim buffer with the current draft prompt; upon exit, reload the buffer contents back into the composer.
|
||||
- Support standard Neovim keybindings and commands (e.g. insert mode, visual mode, plugins) within the embedded pane.
|
||||
- Cleanly restore the previous TUI layout after closing the editor, with prompt focus returned to the composer.
|
||||
- Provide configuration toggle (`embedded_prompt_editor`) and fall back to external-editor prompt behavior when disabled.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Add a new module `tui/src/editor/neovim.rs` that wraps a headless Neovim RPC instance and renders its UI into a dedicated TUI layer.
|
||||
- Extend `tui/src/bottom_pane/chat_composer.rs` to detect `embedded_prompt_editor` and invoke the embedded editor instead of spawning an external process.
|
||||
- Wire a config flag `embedded_prompt_editor: bool` through `ConfigToml` → `Config` under the `tui` section, defaulting to `false`.
|
||||
- Handle Neovim communication via `nvim-rs` crate, multiplexing input/output over the TUI event loop.
|
||||
|
||||
**How it works**
|
||||
- When the user triggers the editor, pause the main TUI rendering and allocate a full-screen or split view for Neovim.
|
||||
- Start Neovim in embedded RPC mode, passing the current prompt text into a new buffer.
|
||||
- Drive Neovim’s UI updates via RPC and render its screen cells into the TUI terminal using termion or similar backend.
|
||||
- Detect the Neovim exit event (e.g. user `:q` or `ZZ`), fetch the buffer contents, and close the embedded view.
|
||||
- Restore the original TUI state and update the composer widget with the edited prompt.
|
||||
|
||||
## Notes
|
||||
|
||||
- This relies on a working `nvim` binary in PATH or specified via `nvim_binary` config.
|
||||
- Investigate performance impact of embedding a full editor in the TUI; ensure fallback to external-editor remains smooth.
|
||||
- Consider edge cases (resizing, plugin‑heavy Neovim configs) and document prerequisites in the README.
|
||||
34
agentydragon/tasks/33-external-editor-focus-issue.md
Normal file
34
agentydragon/tasks/33-external-editor-focus-issue.md
Normal file
@@ -0,0 +1,34 @@
|
||||
+++
|
||||
id = "33"
|
||||
title = "Fix External Editor Focus Issue"
|
||||
status = "Not started"
|
||||
summary = "When launching the external editor from the TUI (e.g. nvim), keyboard input is still captured by the Rust TUI, causing keys to split between the editor and the TUI."
|
||||
dependencies = "06,32" # Rationale: depends on Tasks 06 and 32 for external and embedded editor features
|
||||
last_updated = "2025-06-25T01:40:09.700000"
|
||||
+++
|
||||
|
||||
# Task 33: Fix External Editor Focus Issue
|
||||
|
||||
## Goal
|
||||
Ensure that when the TUI spawns an external editor, it fully hands off keyboard control to the editor, and upon editor exit, restores TUI input handling without leaking keystrokes or misrouting commands.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Launching external editor via `/edit-prompt` or Ctrl+E disables TUI raw mode and event capture so all keystrokes go directly to the editor.
|
||||
- Upon editor exit, raw mode and event capture are correctly re-enabled, and no keystrokes are lost or misrouted.
|
||||
- No residual input events are processed by the TUI while the editor is running.
|
||||
- Add integration tests or manual validation steps simulating editor launch and exit sequences.
|
||||
|
||||
## Implementation
|
||||
|
||||
**High-level plan**
|
||||
- Before spawning the editor process (in `ChatComposer`), call `disable_raw_mode()` and `disable_event_capture()` to restore normal terminal behavior.
|
||||
- Spawn the editor subprocess and wait for it to exit.
|
||||
- After exit, re-enable raw mode and event capture via `enable_raw_mode()` and `enable_event_capture()`.
|
||||
- Wrap this sequence in a helper function (e.g., `spawn_external_editor`) and update the `/edit-prompt` handler to use it.
|
||||
- Add integration tests in `tui/tests/` that mock the editor command (e.g., `echo`) to verify terminal mode transitions.
|
||||
|
||||
## Notes
|
||||
|
||||
- Use Crossterm APIs for terminal mode management.
|
||||
- Ensure interruption signals (e.g., Ctrl+C) during editor sessions are propagated correctly to avoid TUI deadlock.
|
||||
41
agentydragon/tasks/34-set-shell-title-followup.md
Normal file
41
agentydragon/tasks/34-set-shell-title-followup.md
Normal file
@@ -0,0 +1,41 @@
|
||||
+++
|
||||
id = "34"
|
||||
title = "Complete Set Shell Title to Reflect Session Status"
|
||||
status = "Not started"
|
||||
dependencies = "08" # Rationale: depends on Task 08 for initial shell title change
|
||||
last_updated = "2025-06-25T04:45:29Z"
|
||||
+++
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Not started
|
||||
**Summary**: Follow-up to Task 08; implementation missing for core title persistence and ANSI updates.
|
||||
|
||||
## Goal
|
||||
|
||||
Implement the missing pieces from Task 08 to fully support dynamic and persistent shell title updates:
|
||||
1. Define `SessionUpdatedTitleEvent` and add a `title` field in `SessionConfiguredEvent` (core protocol).
|
||||
2. Introduce `Op::SetTitle(String)` variant and handle it in the core agent loop, persisting the title and emitting the update event.
|
||||
3. Update TUI and exec clients to listen for title events and emit ANSI escape sequences (`\x1b]0;<title>\x07`) for live terminal title changes.
|
||||
4. Restore the persisted title on session resume via `SessionConfiguredEvent`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- New `SessionUpdatedTitleEvent` type in `codex_core::protocol` and `title` field in `SessionConfiguredEvent`.
|
||||
- `Op::SetTitle(String)` variant in the protocol and core event handling persisted in session metadata.
|
||||
- Clients broadcast ANSI title-setting sequences on title events and lifecycle state changes.
|
||||
- Unit tests for protocol serialization and client reaction to title updates.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
*(Not implemented yet)*
|
||||
|
||||
**How it works**
|
||||
*(Not implemented yet)*
|
||||
|
||||
## Notes
|
||||
|
||||
- Use ANSI escape code `\x1b]0;<title>\x07` for setting terminal title.
|
||||
39
agentydragon/tasks/36-tests-interactive-prompt-execution.md
Normal file
39
agentydragon/tasks/36-tests-interactive-prompt-execution.md
Normal file
@@ -0,0 +1,39 @@
|
||||
+++
|
||||
id = "36"
|
||||
title = "Add Tests for Interactive Prompting While Executing"
|
||||
status = "Not started"
|
||||
dependencies = "06,13" # Rationale: depends on Tasks 06 and 13 for external editor and interactive prompt support
|
||||
last_updated = "2025-06-25T11:05:55Z"
|
||||
+++
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Done
|
||||
**Summary**: Follow-up to Task 13; add unit tests for interactive prompt overlay during execution.
|
||||
|
||||
## Goal
|
||||
|
||||
Write tests that verify `BottomPane::handle_key_event` forwards input to the composer while `is_task_running`, preserving the status overlay until completion.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Unit tests covering key events (e.g. alphanumeric, Enter) during `is_task_running == true`.
|
||||
- Assertions that `active_view` remains a `StatusIndicatorView` while running and is removed when `set_task_running(false)` is called.
|
||||
- Coverage of redraw requests and correct `InputResult` values.
|
||||
|
||||
## Implementation
|
||||
|
||||
**Planned Approach**
|
||||
|
||||
- Use existing `make_pane` and `make_pane_and_rx` helpers to create a `BottomPane` in a running-task state.
|
||||
- Write unit tests in `tui/src/bottom_pane/mod.rs` that verify:
|
||||
- Typing alphanumeric characters while `is_task_running == true` appends to the composer, maintains the `StatusIndicatorView` overlay, and emits a `AppEvent::Redraw`.
|
||||
- Pressing Enter returns `InputResult::Submitted` with the buffered text, clears the composer, retains the overlay, and triggers a redraw.
|
||||
- Calling `set_task_running(false)` removes the status indicator overlay.
|
||||
- Follow existing patterns from the tests in `user_approval_widget.rs` and `set_title_view.rs`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Refer to existing tests in `user_approval_widget.rs` and `set_title_view.rs` for testing patterns.
|
||||
41
agentydragon/tasks/37-session-state-persistence.md
Normal file
41
agentydragon/tasks/37-session-state-persistence.md
Normal file
@@ -0,0 +1,41 @@
|
||||
+++
|
||||
id = "37"
|
||||
title = "Session State Persistence and Debug Instrumentation"
|
||||
status = "Not started"
|
||||
dependencies = ""
|
||||
last_updated = "2025-06-25T23:00:00.000000"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
Persist session runtime state and capture raw request/response data and supplemental metadata to a session-specific directory.
|
||||
|
||||
## Goal
|
||||
Collect and persist all relevant session state (beyond the rollout transcript) in a dedicated directory under `.codex/sessions/<UUID>/`, to aid debugging and allow post-mortem analysis.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- All session data (transcript, logs, raw OpenAI API requests/responses, approval events, and other runtime metadata) is written under `.codex/sessions/<session_id>/`.
|
||||
- Existing rollout transcript continues to be written to `sessions/rollout-<UUID>.jsonl`, now moved or linked into the session directory.
|
||||
- Logging configuration respects `--debug-log` and writes to the session directory when set to a relative path.
|
||||
- A selector flag (e.g. `--persist-session`) enables or disables writing persistent state.
|
||||
- No change to default behavior when persistence is disabled (i.e. backward compatibility).
|
||||
- Minimal integration test or manual verification steps demonstrate that files appear correctly and no extraneous error logs occur.
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Add a new CLI flag `--persist-session` to the TUI and server binaries to enable session persistence.
|
||||
- Compute a session directory under `$CODEX_HOME/sessions/<UUID>/`, create it at startup when persistence is enabled.
|
||||
- After initializing the rollout file (`rollout-<UUID>.jsonl`), move or symlink it into the session directory.
|
||||
- Configure tracing subscriber file layer and `--debug-log` default path to write logs into the same session directory (e.g. `session.log`).
|
||||
- Instrument the OpenAI HTTP client layer to dump raw request and response bodies into `session_oai_raw.log` in that directory.
|
||||
- In the message sequencing logic, add debug spans to record approval and cancellation events into `session_meta.log`.
|
||||
|
||||
**How it works**
|
||||
- When `--persist-session` is active, all file outputs (rollout transcript, debug logs, raw API dumps, metadata logs) are collated under a single session directory.
|
||||
- If disabled (default), writes occur in the existing locations (`rollout-<UUID>.jsonl`, `$CODEX_HOME/log/`), preserving current behavior.
|
||||
|
||||
## Notes
|
||||
|
||||
- This feature streamlines troubleshooting by co-locating all session artifacts.
|
||||
- Ensure directory creation and file writes handle permission errors gracefully and fallback cleanly when disabled.
|
||||
35
agentydragon/tasks/39-patch-diff-left-indent-coloring-fix.md
Normal file
35
agentydragon/tasks/39-patch-diff-left-indent-coloring-fix.md
Normal file
@@ -0,0 +1,35 @@
|
||||
+++
|
||||
id = "39"
|
||||
title = "Fix Coloring of Left-Indented Patch Diffs"
|
||||
status = "Not started"
|
||||
dependencies = ""
|
||||
summary = "Patch diffs rendered with left indentation mode are not colored correctly, losing syntax highlighting."
|
||||
last_updated = "2025-06-25T00:00:00Z"
|
||||
+++
|
||||
|
||||
# Task 39: Fix Coloring of Left-Indented Patch Diffs
|
||||
|
||||
> *UI bug:* When patch diffs are rendered in left-indented mode, the ANSI color codes are misaligned, resulting in lost or incorrect coloring.
|
||||
|
||||
## Status
|
||||
|
||||
**General Status**: Not started
|
||||
**Summary**: Diagnose offset logic in diff renderer and adjust color processing to account for indentation.
|
||||
|
||||
## Goal
|
||||
|
||||
Ensure diff lines maintain proper ANSI color highlighting even when indented on the left by a fixed margin.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Diff render tests pass for both default and indented modes.
|
||||
- Visual manual check confirms colored diff alignment.
|
||||
|
||||
## Implementation
|
||||
|
||||
- Update diff renderer to strip indentation before applying color logic, then reapply indentation.
|
||||
- Add unit tests for multiline indented diffs.
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Any implementation notes -->
|
||||
34
agentydragon/tasks/40-cli-input-multiline-paste.md
Normal file
34
agentydragon/tasks/40-cli-input-multiline-paste.md
Normal file
@@ -0,0 +1,34 @@
|
||||
+++
|
||||
id = "40"
|
||||
title = "Support Multiline Paste in codex-rs CLI Input Window"
|
||||
status = "Not started"
|
||||
freeform_status = ""
|
||||
dependencies = ""
|
||||
last_updated = "2025-06-25T09:19:34Z"
|
||||
+++
|
||||
|
||||
# Task 40: Support Multiline Paste in codex-rs CLI Input Window
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- When pasting multiline text into the codex-rs CLI input (REPL), newlines in the pasted text are inserted into the input buffer rather than causing premature command execution.
|
||||
- The pasted content preserves original end-of-line characters and spacing.
|
||||
- The user can still press Enter to submit the complete command when desired.
|
||||
- Behavior for single-line input and manual line breaks remains unchanged.
|
||||
|
||||
## Implementation
|
||||
**How it was implemented**
|
||||
Provide details on code modules, design decisions, and steps taken.
|
||||
*If this section is left blank or contains only placeholder text, the implementing developer should first populate it with a concise high-level plan before writing code.*
|
||||
|
||||
**How it works**
|
||||
Explain runtime behavior and overall operation.
|
||||
*If this section is left blank or contains only placeholder text, the implementing developer should update it to describe the intended runtime behavior.*
|
||||
|
||||
## Notes
|
||||
|
||||
- Investigate enabling bracketed paste support in the line-editing library used (e.g. rustyline, liner).
|
||||
- Ensure that bracketed paste mode is enabled when initializing the CLI to distinguish between pasted content and typed input.
|
||||
- Review how other REPLs implement multiline paste handling to inform the design.
|
||||
29
agentydragon/tasks/41-slash-init-command.md
Normal file
29
agentydragon/tasks/41-slash-init-command.md
Normal file
@@ -0,0 +1,29 @@
|
||||
+++
|
||||
id = "41"
|
||||
title = "Slash-command /init to load init prompt into composer"
|
||||
status = "Not started"
|
||||
freeform_status = ""
|
||||
dependencies = ""
|
||||
last_updated = "2025-06-25T11:23:30Z"
|
||||
+++
|
||||
|
||||
# Task 41: Slash-command /init to load init prompt into composer
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Typing `/init` in the chat composer should load the contents of `codex-rs/code/init.md` into the input buffer.
|
||||
- `/init` appears in the slash-command menu alongside other commands.
|
||||
- After executing `/init`, the composer shows the init prompt, ready for editing.
|
||||
|
||||
## Implementation
|
||||
|
||||
- Add a new slash-command identifier `/init` in the command dispatch logic (e.g. in `ChatComposer` or equivalent).
|
||||
- On `/init`, read `codex-rs/code/init.md` (relative to the repository root) and inject its text into the composer buffer.
|
||||
- Ensure the slash-menu and feedback UI treat `/init` consistently with other commands.
|
||||
- Write unit tests to verify that `/init` populates the composer correctly without losing focus.
|
||||
|
||||
## Notes
|
||||
|
||||
Link to the init prompt source: `codex-rs/code/init.md`.
|
||||
32
agentydragon/tasks/task-template.md
Normal file
32
agentydragon/tasks/task-template.md
Normal file
@@ -0,0 +1,32 @@
|
||||
+++
|
||||
id = "<NN>"
|
||||
title = "<Task Title>"
|
||||
status = "<<<!!! MANAGER: SET VALID STATUS - Not started? !!!>>>"
|
||||
freeform_status = "<<<!!! MANAGER/DEVELOPER: Freeform status text, optional. E.g. progress notes or developer comments. !!!>>>"
|
||||
dependencies = [<<<!!! MANAGER: LIST TASK IDS THAT MUST BE COMPLETED BEFORE STARTING; SEPARATED BY COMMAS, E.G. "02","05" !!!>>>] # <!-- Manager rationale: explain why these dependencies are required and why other tasks are not. -->
|
||||
last_updated = "<timestamp in ISO format>"
|
||||
+++
|
||||
|
||||
# Task Template
|
||||
|
||||
# Valid status values: Not started | In progress | Needs input | Needs manual review | Done | Cancelled | Merged
|
||||
|
||||
|
||||
> *This task is specific to codex-rs.*
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
List measurable criteria for completion.
|
||||
|
||||
## Implementation
|
||||
**How it was implemented**
|
||||
Provide details on code modules, design decisions, and steps taken.
|
||||
*If this section is left blank or contains only placeholder text, the implementing developer should first populate it with a concise high-level plan before writing code.*
|
||||
|
||||
**How it works**
|
||||
Explain runtime behavior and overall operation.
|
||||
*If this section is left blank or contains only placeholder text, the implementing developer should update it to describe the intended runtime behavior.*
|
||||
|
||||
## Notes
|
||||
|
||||
Any additional notes or references.
|
||||
126
agentydragon/tools/check_tasks.py
Normal file
126
agentydragon/tools/check_tasks.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
check_tasks.py: Run all task-directory validation checks in one go.
|
||||
- Ensure task Markdown frontmatter parses and validates (id, title, status, etc.).
|
||||
- Detect circular dependencies among non-merged tasks.
|
||||
- Enforce only .md files under agentydragon/tasks/ (excluding .worktrees/ and .done/).
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from manager_utils.tasklib import task_dir, worktree_dir, load_task
|
||||
|
||||
|
||||
def skip_path(p: Path) -> bool:
|
||||
"""Return True for paths we should ignore in validations."""
|
||||
wt = worktree_dir()
|
||||
done = task_dir() / ".done"
|
||||
if p.is_relative_to(wt) or p.is_relative_to(done):
|
||||
return True
|
||||
if p.name in ("task-template.md",) or p.name.endswith("-plan.md"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def iter_task_markdown() -> Path:
|
||||
"""Yield all task markdown files under agentydragon/tasks, pruning .worktrees and .done dirs."""
|
||||
wt = worktree_dir()
|
||||
done = task_dir() / ".done"
|
||||
root = task_dir()
|
||||
for base, dirs, files in os.walk(str(root)):
|
||||
# do not descend into .worktrees or .done
|
||||
dirs[:] = [d for d in dirs if (Path(base) / d) not in (wt, done)]
|
||||
for fn in files:
|
||||
if re.fullmatch(r"[0-9]{2}-.*\.md", fn):
|
||||
yield Path(base) / fn
|
||||
|
||||
|
||||
def check_file_types():
|
||||
failures: list[Path] = []
|
||||
for p in task_dir().iterdir():
|
||||
if skip_path(p) or p.is_dir():
|
||||
continue
|
||||
if p.suffix.lower() != ".md":
|
||||
failures.append(p)
|
||||
return failures
|
||||
|
||||
|
||||
def check_frontmatter():
|
||||
failures: list[tuple[Path, str]] = []
|
||||
for md in iter_task_markdown():
|
||||
try:
|
||||
load_task(md)
|
||||
except Exception as e:
|
||||
failures.append((md, str(e)))
|
||||
return failures
|
||||
|
||||
|
||||
def check_cycles():
|
||||
merged = set()
|
||||
deps_map: dict[str, list[str]] = {}
|
||||
for md in iter_task_markdown():
|
||||
meta, _ = load_task(md)
|
||||
if meta.status == "Merged":
|
||||
merged.add(meta.id)
|
||||
else:
|
||||
deps = [d for d in re.findall(r"\d+", meta.dependencies)]
|
||||
deps_map[meta.id] = [d for d in deps if d not in merged]
|
||||
|
||||
failures: list[list[str]] = []
|
||||
visited: set[str] = set()
|
||||
stack: list[str] = []
|
||||
|
||||
def visit(n: str):
|
||||
if n in stack:
|
||||
cycle = stack[stack.index(n) :] + [n]
|
||||
failures.append(cycle)
|
||||
return
|
||||
if n in visited:
|
||||
return
|
||||
stack.append(n)
|
||||
for m in deps_map.get(n, []):
|
||||
visit(m)
|
||||
stack.pop()
|
||||
visited.add(n)
|
||||
|
||||
for node in deps_map:
|
||||
visit(node)
|
||||
return failures
|
||||
|
||||
|
||||
def main():
|
||||
err = False
|
||||
|
||||
# File type check
|
||||
ft_fail = check_file_types()
|
||||
if ft_fail:
|
||||
print("Non-md files under tasks/:", file=sys.stderr)
|
||||
for f in ft_fail:
|
||||
print(f" {f}", file=sys.stderr)
|
||||
err = True
|
||||
|
||||
# Frontmatter check
|
||||
fm_fail = check_frontmatter()
|
||||
if fm_fail:
|
||||
print("\nFrontmatter errors:", file=sys.stderr)
|
||||
for md, msg in fm_fail:
|
||||
print(f" {md}: {msg}", file=sys.stderr)
|
||||
err = True
|
||||
|
||||
# Dependency cycles
|
||||
cyc_fail = check_cycles()
|
||||
if cyc_fail:
|
||||
print("\nCircular dependency errors:", file=sys.stderr)
|
||||
for cycle in cyc_fail:
|
||||
print(" " + " -> ".join(cycle), file=sys.stderr)
|
||||
err = True
|
||||
|
||||
if err:
|
||||
sys.exit(1)
|
||||
print("All task checks passed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
32
agentydragon/tools/common.py
Normal file
32
agentydragon/tools/common.py
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
common.py: Shared utilities for agentydragon tooling scripts.
|
||||
"""
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
"""Return the Git repository root directory."""
|
||||
out = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'])
|
||||
return Path(out.decode().strip())
|
||||
|
||||
|
||||
def tasks_dir() -> Path:
|
||||
"""Path to the agentydragon/tasks directory."""
|
||||
return repo_root() / 'agentydragon' / 'tasks'
|
||||
|
||||
|
||||
def worktrees_dir() -> Path:
|
||||
"""Path to the agentydragon/tasks/.worktrees directory."""
|
||||
return tasks_dir() / '.worktrees'
|
||||
|
||||
|
||||
def resolve_slug(input_id: str) -> str:
|
||||
"""Resolve a two-digit task ID into its full slug, or return slug unchanged."""
|
||||
if input_id.isdigit() and len(input_id) == 2:
|
||||
matches = list(tasks_dir().glob(f"{input_id}-*.md"))
|
||||
if len(matches) == 1:
|
||||
return matches[0].stem
|
||||
raise ValueError(f"Expected one task file for ID {input_id}, found {len(matches)}")
|
||||
return input_id
|
||||
153
agentydragon/tools/create_task_worktree.py
Normal file
153
agentydragon/tools/create_task_worktree.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
create_task_worktree.py: Create or reuse a git worktree for a specific task and optionally launch a Developer Codex agent.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from common import repo_root, tasks_dir, worktrees_dir, resolve_slug
|
||||
|
||||
|
||||
def run(cmd, cwd=None):
|
||||
click.echo(f"Running: {' '.join(cmd)}")
|
||||
subprocess.check_call(cmd, cwd=cwd)
|
||||
|
||||
|
||||
def resolve_slug(input_id: str) -> str:
|
||||
if input_id.isdigit() and len(input_id) == 2:
|
||||
matches = list(tasks_dir().glob(f"{input_id}-*.md"))
|
||||
if len(matches) == 1:
|
||||
return matches[0].stem
|
||||
click.echo(f"Error: expected one task file for ID {input_id}, found {len(matches)}", err=True)
|
||||
sys.exit(1)
|
||||
return input_id
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('-a', '--agent', is_flag=True,
|
||||
help='Launch Developer Codex agent after setting up worktree.')
|
||||
@click.option('-t', '--tmux', 'tmux_mode', is_flag=True,
|
||||
help='Open each task in its own tmux pane; implies --agent. '
|
||||
'Attaches to an existing session if already running.')
|
||||
@click.option('-i', '--interactive', is_flag=True,
|
||||
help='Run agent in interactive mode (no exec); implies --agent.')
|
||||
@click.option('-s', '--shell', 'shell_mode', is_flag=True,
|
||||
help='Launch an interactive Codex shell (skip exec and auto-commit); implies --agent and --interactive.')
|
||||
@click.option('--skip-presubmit', is_flag=True,
|
||||
help='Skip the initial presubmit pre-commit checks when creating a new worktree.')
|
||||
@click.argument('task_inputs', nargs=-1, required=True)
|
||||
def main(agent, tmux_mode, interactive, shell_mode, skip_presubmit, task_inputs):
|
||||
"""Create/reuse a task worktree and optionally launch a Dev agent or tmux session."""
|
||||
# shell mode implies interactive (skip exec within the worktree)
|
||||
if shell_mode:
|
||||
interactive = True
|
||||
if interactive or shell_mode:
|
||||
agent = True
|
||||
|
||||
if tmux_mode:
|
||||
agent = True
|
||||
session = 'agentydragon_' + '_'.join(task_inputs)
|
||||
# If a tmux session already exists, skip setup and attach
|
||||
if subprocess.call(['tmux', 'has-session', '-t', session]) == 0:
|
||||
click.echo(f"Session {session} already exists; attaching")
|
||||
run(['tmux', 'attach', '-t', session])
|
||||
return
|
||||
# Create a new session and windows for each task
|
||||
for idx, inp in enumerate(task_inputs):
|
||||
slug = resolve_slug(inp)
|
||||
cmd = [sys.executable, '-u', __file__]
|
||||
if agent:
|
||||
cmd.append('--agent')
|
||||
cmd.append(slug)
|
||||
if idx == 0:
|
||||
run(['tmux', 'new-session', '-d', '-s', session] + cmd)
|
||||
else:
|
||||
run(['tmux', 'new-window', '-t', session] + cmd)
|
||||
run(['tmux', 'attach', '-t', session])
|
||||
return
|
||||
|
||||
# Single task
|
||||
slug = resolve_slug(task_inputs[0])
|
||||
branch = f"agentydragon-{slug}"
|
||||
wt_root = worktrees_dir()
|
||||
wt_path = wt_root / slug
|
||||
|
||||
# Ensure branch exists
|
||||
if subprocess.call(['git', 'show-ref', '--verify', '--quiet', f'refs/heads/{branch}']) != 0:
|
||||
run(['git', 'branch', '--track', branch, 'agentydragon'])
|
||||
|
||||
wt_root.mkdir(parents=True, exist_ok=True)
|
||||
new_wt = False
|
||||
if not wt_path.exists():
|
||||
# --- COW hydration logic via rsync ---
|
||||
# Instead of checking out files normally, register the worktree empty and then
|
||||
# perform a filesystem-level hydration via rsync (with reflink if supported) for
|
||||
# near-instant setup while excluding VCS metadata and other worktrees.
|
||||
run(['git', 'worktree', 'add', '--no-checkout', str(wt_path), branch])
|
||||
src = str(repo_root())
|
||||
dst = str(wt_path)
|
||||
# Hydrate the worktree filesystem via rsync, excluding .git and any .worktrees to avoid recursion
|
||||
rsync_cmd = [
|
||||
'rsync', '-a', '--delete', f'{src}/', f'{dst}/',
|
||||
'--exclude=.git*', '--exclude=.worktrees/'
|
||||
]
|
||||
if sys.platform != 'darwin':
|
||||
rsync_cmd.insert(3, '--reflink=auto')
|
||||
run(rsync_cmd)
|
||||
# Install pre-commit hooks in the new worktree
|
||||
if shutil.which('pre-commit'):
|
||||
run(['pre-commit', 'install'], cwd=dst)
|
||||
else:
|
||||
click.echo('Warning: pre-commit not found; skipping hook install', err=True)
|
||||
new_wt = True
|
||||
else:
|
||||
click.echo(f'Worktree already exists at {wt_path}')
|
||||
|
||||
if not agent:
|
||||
return
|
||||
|
||||
# Initial presubmit: only on new worktree & branch, unless skipped or in shell mode
|
||||
if new_wt and not skip_presubmit and not shell_mode:
|
||||
if shutil.which('pre-commit'):
|
||||
try:
|
||||
run(['pre-commit', 'run', '--all-files'], cwd=str(wt_path))
|
||||
except subprocess.CalledProcessError:
|
||||
click.echo(
|
||||
'Pre-commit checks failed. Please fix the issues in the worktree or ' +
|
||||
're-run with --skip-presubmit to bypass these checks.', err=True)
|
||||
sys.exit(1)
|
||||
else:
|
||||
click.echo('Warning: pre-commit not installed; skipping presubmit checks', err=True)
|
||||
|
||||
click.echo(f'Launching Developer Codex agent for task {slug} in sandboxed worktree')
|
||||
|
||||
click.echo(f'Launching Developer Codex agent for task {slug} in sandboxed worktree')
|
||||
os.chdir(wt_path)
|
||||
cmd = ['codex', '--full-auto']
|
||||
if not interactive:
|
||||
cmd.append('exec')
|
||||
prompt = (repo_root() / 'agentydragon' / 'prompts' / 'developer.md').read_text()
|
||||
taskfile = (tasks_dir() / f'{slug}.md').read_text()
|
||||
run(cmd + [prompt + '\n\n' + taskfile])
|
||||
# After Developer agent exits, if task status is Done, invoke Commit agent to stage and commit changes
|
||||
task_path = tasks_dir() / f"{slug}.md"
|
||||
content = task_path.read_text(encoding='utf-8')
|
||||
m = re.search(r'^status\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
status = m.group(1) if m else None
|
||||
if status and status.lower() == 'done':
|
||||
click.echo(f"Task {slug} marked Done; running Commit agent helper")
|
||||
commit_script = repo_root() / 'agentydragon' / 'tools' / 'launch_commit_agent.py'
|
||||
# Launch commit agent from the main repo root, not inside the task worktree
|
||||
run([sys.executable, str(commit_script), slug], cwd=str(repo_root()))
|
||||
else:
|
||||
click.echo(f"Task {slug} status is '{status or 'unknown'}'; skipping Commit agent helper")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import shutil
|
||||
main()
|
||||
57
agentydragon/tools/launch_commit_agent.py
Normal file
57
agentydragon/tools/launch_commit_agent.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
launch_commit_agent.py: Run the non-interactive Commit agent for completed tasks.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from common import repo_root, tasks_dir, worktrees_dir, resolve_slug
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument('task_input', required=True)
|
||||
def main(task_input):
|
||||
"""Resolve TASK_INPUT to slug, run the Commit agent, and commit changes."""
|
||||
slug = resolve_slug(task_input)
|
||||
wt = worktrees_dir() / slug
|
||||
if not wt.exists():
|
||||
click.echo(f"Error: worktree for '{slug}' not found; run create_task_worktree.py first", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
prompt_file = repo_root() / 'agentydragon' / 'prompts' / 'commit.md'
|
||||
task_file = tasks_dir() / f'{slug}.md'
|
||||
for f in (prompt_file, task_file):
|
||||
if not f.exists():
|
||||
click.echo(f"Error: file not found: {f}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
msg_file = Path(subprocess.check_output(['mktemp']).decode().strip())
|
||||
try:
|
||||
os.chdir(wt)
|
||||
# Abort early if no pending changes in this worktree
|
||||
status_out = subprocess.check_output(['git', 'status', '--porcelain'], text=True).strip()
|
||||
if not status_out:
|
||||
click.echo(f"No changes detected in worktree for '{slug}'; nothing to commit.", err=True)
|
||||
sys.exit(0)
|
||||
cmd = ['codex', '--full-auto', 'exec', '--output-last-message', str(msg_file)]
|
||||
# Run the Commit agent in silent mode (suppressing its full stdout)
|
||||
click.echo(f"Running commit agent: {' '.join(cmd)}")
|
||||
prompt_content = prompt_file.read_text(encoding='utf-8')
|
||||
task_content = task_file.read_text(encoding='utf-8')
|
||||
subprocess.check_call(cmd + [prompt_content + '\n\n' + task_content], stdout=subprocess.DEVNULL)
|
||||
# Stage all changes, including new files (not just modifications)
|
||||
subprocess.check_call(['git', 'add', '-A'])
|
||||
subprocess.check_call(['git', 'commit', '-F', str(msg_file)])
|
||||
# Print the commit message for visibility
|
||||
msg = msg_file.read_text(encoding='utf-8').strip()
|
||||
click.echo("Commit message:\n" + msg)
|
||||
finally:
|
||||
msg_file.unlink()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
28
agentydragon/tools/launch_project_manager.py
Normal file
28
agentydragon/tools/launch_project_manager.py
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
launch_project_manager.py: Launch the Codex Project Manager agent prompt.
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import click
|
||||
|
||||
from common import repo_root
|
||||
|
||||
|
||||
@click.command()
|
||||
def main():
|
||||
"""Read manager.md prompt and invoke Codex Project Manager agent."""
|
||||
prompt_file = repo_root() / 'agentydragon' / 'prompts' / 'manager.md'
|
||||
if not prompt_file.exists():
|
||||
click.echo(f"Error: manager prompt not found at {prompt_file}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
prompt = prompt_file.read_text(encoding='utf-8')
|
||||
cmd = ['codex', prompt]
|
||||
click.echo(f"Running: {' '.join(cmd[:1])} <prompt>")
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
9
agentydragon/tools/manager_utils/README.md
Normal file
9
agentydragon/tools/manager_utils/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# manager_utils
|
||||
|
||||
This directory contains utility scripts to support the Project Manager agent.
|
||||
Scripts here should automate common manager tasks (e.g. scanning branches for status,
|
||||
checking for merge conflicts, proposing merges, conflict-resolution guidance,
|
||||
polling loops, etc.).
|
||||
|
||||
Each script should include a short header explaining its purpose, usage examples,
|
||||
and any dependencies.
|
||||
4
agentydragon/tools/manager_utils/__init__.py
Normal file
4
agentydragon/tools/manager_utils/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
agentydragon manager utilities package.
|
||||
"""
|
||||
__version__ = '0.1'
|
||||
355
agentydragon/tools/manager_utils/agentydragon_task.py
Normal file
355
agentydragon/tools/manager_utils/agentydragon_task.py
Normal file
@@ -0,0 +1,355 @@
|
||||
"""
|
||||
CLI for managing agentydragon tasks: status, set-status, set-deps, dispose, launch.
|
||||
"""
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import click
|
||||
from tasklib import load_task, repo_root, save_task, task_dir, TaskMeta, worktree_dir, TaskStatus
|
||||
import shutil
|
||||
|
||||
try:
|
||||
from tabulate import tabulate
|
||||
except ImportError:
|
||||
tabulate = None
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
"""Manage agentydragon tasks."""
|
||||
pass
|
||||
|
||||
@cli.command()
|
||||
def status():
|
||||
"""Show a table of task id, title, status, dependencies, last_updated.
|
||||
|
||||
If tabulate is installed, render as GitHub-flavored Markdown table;
|
||||
otherwise fallback to fixed-width formatting.
|
||||
"""
|
||||
# Load all task metadata, reporting load errors with file path
|
||||
all_meta: dict[str, TaskMeta] = {}
|
||||
path_map: dict[str, Path] = {}
|
||||
wt_root = worktree_dir()
|
||||
for md in sorted(task_dir().rglob('[0-9][0-9]-*.md')):
|
||||
# skip task template, plan files, and any worktree copies
|
||||
if md.name in ('task-template.md',) or md.name.endswith('-plan.md') or md.is_relative_to(wt_root):
|
||||
continue
|
||||
try:
|
||||
meta, _ = load_task(md)
|
||||
except Exception as e:
|
||||
print(f"Error loading {md}: {e}")
|
||||
continue
|
||||
all_meta[meta.id] = meta
|
||||
path_map[meta.id] = md
|
||||
|
||||
# If a worktree exists, reload the task from that workspace (including .done paths)
|
||||
repo = repo_root()
|
||||
for tid, md in list(path_map.items()):
|
||||
wt_root_dir = wt_root / md.stem
|
||||
# derive relative path of the task file under the repo
|
||||
try:
|
||||
rel = md.relative_to(repo)
|
||||
except Exception:
|
||||
continue
|
||||
wt_task = wt_root_dir / rel
|
||||
if wt_task.exists():
|
||||
try:
|
||||
wt_meta, _ = load_task(wt_task)
|
||||
all_meta[tid] = wt_meta
|
||||
path_map[tid] = wt_task
|
||||
except Exception as e:
|
||||
print(f"Error loading {wt_task}: {e}")
|
||||
|
||||
# Build dependency graph, excluding already merged tasks
|
||||
merged_ids = {tid for tid, m in all_meta.items() if m.status == 'Merged'}
|
||||
deps_map: dict[str, list[str]] = {}
|
||||
for tid, meta in all_meta.items():
|
||||
deps_map[tid] = [d for d in re.findall(r"\d+", meta.dependencies)
|
||||
if d in all_meta and d not in merged_ids]
|
||||
|
||||
# Topologically sort tasks by dependencies, fall back on filename order on error
|
||||
try:
|
||||
sorted_ids: list[str] = []
|
||||
temp: set[str] = set()
|
||||
perm: set[str] = set()
|
||||
def visit(n: str) -> None:
|
||||
if n in perm:
|
||||
return
|
||||
if n in temp:
|
||||
raise RuntimeError(f"Circular dependency detected at task {n}")
|
||||
temp.add(n)
|
||||
for m in deps_map.get(n, []):
|
||||
visit(m)
|
||||
temp.remove(n)
|
||||
perm.add(n)
|
||||
sorted_ids.append(n)
|
||||
for n in all_meta:
|
||||
visit(n)
|
||||
except Exception as e:
|
||||
print(f"Warning: cannot topo-sort tasks ({e}); falling back to filename order")
|
||||
sorted_ids = [m.id for m in sorted(all_meta.values(), key=lambda m: path_map[m.id].name)]
|
||||
|
||||
# Identify tasks that are merged with no branch and no worktree (bottom summary)
|
||||
bottom_merged_ids: set[str] = set()
|
||||
for tid in sorted_ids:
|
||||
meta = all_meta[tid]
|
||||
if meta.status != 'Merged':
|
||||
continue
|
||||
branches = subprocess.run(
|
||||
['git', 'for-each-ref', '--format=%(refname:short)',
|
||||
f'refs/heads/agentydragon-{tid}-*'],
|
||||
capture_output=True, text=True, cwd=repo_root()
|
||||
).stdout.strip().splitlines()
|
||||
wt_dir = task_dir() / '.worktrees' / path_map[tid].stem
|
||||
if not branches and not wt_dir.exists():
|
||||
bottom_merged_ids.add(tid)
|
||||
|
||||
rows: list[tuple] = []
|
||||
merged_tasks: list[tuple[str, str]] = []
|
||||
root = repo_root()
|
||||
|
||||
for tid in sorted_ids:
|
||||
meta = all_meta[tid]
|
||||
md = path_map[tid]
|
||||
slug = md.stem
|
||||
# branch detection
|
||||
branches = subprocess.run(
|
||||
['git', 'for-each-ref', '--format=%(refname:short)',
|
||||
f'refs/heads/agentydragon-{tid}-*'],
|
||||
capture_output=True, text=True, cwd=root
|
||||
).stdout.strip().splitlines()
|
||||
branch_exists = 'Y' if branches and branches[0].strip() else 'N'
|
||||
merged_flag = 'N'
|
||||
if branch_exists == 'Y':
|
||||
b = branches[0].lstrip('*+ ').strip()
|
||||
if subprocess.run(['git', 'merge-base', '--is-ancestor', b, 'agentydragon'], cwd=root).returncode == 0:
|
||||
merged_flag = 'Y'
|
||||
# worktree detection
|
||||
wt_dir = worktree_dir() / slug
|
||||
wt_info = 'none'
|
||||
if wt_dir.exists():
|
||||
st = subprocess.run(['git', 'status', '--porcelain'], cwd=wt_dir,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
wt_info = 'clean' if not st else 'dirty'
|
||||
|
||||
# skip fully merged tasks (no branch, no worktree)
|
||||
if meta.status == 'Merged' and branch_exists == 'N' and wt_info == 'none':
|
||||
merged_tasks.append((tid, meta.title))
|
||||
continue
|
||||
|
||||
# filter out dependencies on bottom-summary merged tasks
|
||||
deps = [d for d in deps_map.get(tid, []) if d not in bottom_merged_ids]
|
||||
deps_str = ','.join(deps)
|
||||
|
||||
# determine branch_info text
|
||||
if branch_exists == 'N':
|
||||
branch_info = 'no branch'
|
||||
elif merged_flag == 'Y':
|
||||
branch_info = 'merged'
|
||||
else:
|
||||
a_cnt, b_cnt = subprocess.check_output(
|
||||
['git', 'rev-list', '--left-right', '--count',
|
||||
f'{branches[0]}...agentydragon'], cwd=root
|
||||
).decode().split()
|
||||
# compact diffstat: e.g. "56 files changed, 1265 insertions(+), 342 deletions(-)" -> "56f,1265i,342d"
|
||||
raw = subprocess.check_output(
|
||||
['git', 'diff', '--shortstat', f'{branches[0]}...agentydragon'], cwd=root
|
||||
).decode().strip()
|
||||
stat = (
|
||||
raw.replace(' files changed', 'f')
|
||||
.replace(' file changed', 'f')
|
||||
.replace(' insertions(+)', 'i')
|
||||
.replace(' deletions(-)', 'd')
|
||||
.replace(', ', ',')
|
||||
)
|
||||
base = subprocess.check_output(
|
||||
['git', 'merge-base', 'agentydragon', branches[0]], cwd=root
|
||||
).decode().strip()
|
||||
mtree = subprocess.check_output(
|
||||
['git', 'merge-tree', base, 'agentydragon', branches[0]], cwd=root
|
||||
).decode(errors='ignore')
|
||||
conflict = 'conflict' if '<<<<<<<' in mtree else 'ok'
|
||||
if a_cnt == '0' and b_cnt == '0':
|
||||
branch_info = f'up-to-date (+{stat or 0})'
|
||||
else:
|
||||
branch_info = f'{b_cnt} behind / {a_cnt} ahead (+{stat or 0}) {conflict}'
|
||||
|
||||
# Use the human-readable enum value and apply a color map
|
||||
label = meta.status.value
|
||||
status_colors = {
|
||||
'Not started': '\033[90m', # dim gray
|
||||
'In progress': '\033[33m', # yellow
|
||||
'Needs input': '\033[31m', # red
|
||||
'Needs manual review': '\033[31m', # red
|
||||
'Done': '\033[32m', # green
|
||||
'Cancelled': '\033[31m', # red
|
||||
'Merged': '\033[34m', # blue
|
||||
}
|
||||
color = status_colors.get(label, '')
|
||||
stat_disp = f"{color}{label}\033[0m" if color else label
|
||||
wt_disp = wt_info
|
||||
if wt_info == 'dirty':
|
||||
wt_disp = f"\033[31m{wt_info}\033[0m"
|
||||
|
||||
rows.append((
|
||||
tid, meta.title, stat_disp,
|
||||
deps_str, meta.last_updated.strftime('%Y-%m-%d %H:%M'),
|
||||
branch_info, wt_disp
|
||||
))
|
||||
|
||||
headers = ['ID', 'Title', 'Status', 'Dependencies', 'Updated',
|
||||
'Branch Status', 'Worktree Status']
|
||||
if tabulate:
|
||||
print(tabulate(rows, headers=headers, tablefmt='github'))
|
||||
else:
|
||||
fmt = '{:>2} {:<30} {:<12} {:<20} {:<16} {:<40} {:<10}'
|
||||
print(fmt.format(*headers))
|
||||
for r in rows:
|
||||
print(fmt.format(*r))
|
||||
|
||||
# summary of fully merged tasks (no branch, no worktree)
|
||||
if merged_tasks:
|
||||
items = ' '.join(f"{tid} ({title})" for tid, title in merged_tasks)
|
||||
print(f"\n\033[32mMerged:\033[0m {items}")
|
||||
|
||||
# summary of tasks Ready to merge (Done with branch commits)
|
||||
ready_tasks: list[tuple[str, str]] = []
|
||||
for tid in sorted_ids:
|
||||
meta = all_meta[tid]
|
||||
if meta.status != 'Done':
|
||||
continue
|
||||
# detect branch existence and ahead commits
|
||||
branches = subprocess.run(
|
||||
['git', 'for-each-ref', '--format=%(refname:short)', f'refs/heads/agentydragon-{tid}-*'],
|
||||
capture_output=True, text=True, cwd=repo_root()
|
||||
).stdout.strip().splitlines()
|
||||
if not branches or not branches[0].strip():
|
||||
continue
|
||||
bname = branches[0].lstrip('*+ ').strip()
|
||||
# count commits ahead of integration branch
|
||||
a_cnt, _b_cnt = subprocess.check_output(
|
||||
['git', 'rev-list', '--left-right', '--count', f'{bname}...agentydragon'], cwd=repo_root()
|
||||
).decode().split()
|
||||
if int(a_cnt) > 0:
|
||||
ready_tasks.append((tid, meta.title))
|
||||
if ready_tasks:
|
||||
items = ' '.join(f"{tid} ({title})" for tid, title in ready_tasks)
|
||||
print(f"\n\033[33mReady to merge:\033[0m {items}")
|
||||
|
||||
# identify unblocked tasks (no remaining dependencies)
|
||||
unblocked = [tid for tid in sorted_ids if tid not in merged_ids and not deps_map.get(tid)]
|
||||
if unblocked:
|
||||
print(f"\n\033[1mUnblocked:\033[0m {' '.join(unblocked)}")
|
||||
print(f"\033[1mLaunch unblocked in tmux:\033[0m python agentydragon/tools/create_task_worktree.py --agent --tmux {' '.join(unblocked)}")
|
||||
|
||||
@cli.command()
|
||||
@click.argument('task_id')
|
||||
@click.argument('status')
|
||||
def set_status(task_id, status):
|
||||
"""Set status of TASK_ID to STATUS"""
|
||||
# search both in tasks/ and tasks/.done/ for the task file
|
||||
files = list(task_dir().rglob(f'{task_id}-*.md'))
|
||||
if not files:
|
||||
click.echo(f'Task {task_id} not found', err=True)
|
||||
sys.exit(1)
|
||||
path = files[0]
|
||||
meta, body = load_task(path)
|
||||
meta.status = status
|
||||
meta.last_updated = datetime.utcnow()
|
||||
save_task(path, meta, body)
|
||||
# Move between tasks/ and tasks/.done according to status transitions
|
||||
done_dir = task_dir() / '.done'
|
||||
# Move to .done on Merged
|
||||
if meta.status == TaskStatus.MERGED and path.parent.name != '.done':
|
||||
done_dir.mkdir(exist_ok=True)
|
||||
dest = done_dir / path.name
|
||||
click.echo(f"Archiving task: moving {path.name} -> {done_dir.relative_to(repo_root())}")
|
||||
subprocess.run(['git', 'mv', str(path), str(dest)], cwd=repo_root())
|
||||
# Move back to main tasks/ when status changes away from Done/Merged
|
||||
elif path.parent.name == '.done' and meta.status not in (TaskStatus.DONE, TaskStatus.MERGED):
|
||||
dest = task_dir() / path.name
|
||||
click.echo(f"Reopening task: moving {path.name} -> {dest.parent.relative_to(repo_root())}")
|
||||
subprocess.run(['git', 'mv', str(path), str(dest)], cwd=repo_root())
|
||||
|
||||
@cli.command()
|
||||
@click.argument('task_id')
|
||||
@click.argument('deps', nargs=-1)
|
||||
def set_deps(task_id, deps):
|
||||
"""Set dependencies of TASK_ID"""
|
||||
files = list(task_dir().glob(f'{task_id}-*.md'))
|
||||
if not files:
|
||||
click.echo(f'Task {task_id} not found', err=True)
|
||||
sys.exit(1)
|
||||
path = files[0]
|
||||
meta, body = load_task(path)
|
||||
now = datetime.utcnow().isoformat()
|
||||
meta.dependencies = f'as of {now}: ' + ', '.join(deps)
|
||||
meta.last_updated = datetime.utcnow()
|
||||
save_task(path, meta, body)
|
||||
|
||||
@cli.command()
|
||||
@click.argument('task_id', nargs=-1)
|
||||
def dispose(task_id):
|
||||
"""Dispose worktree and delete branch for TASK_ID(s)"""
|
||||
root = repo_root()
|
||||
wt_base = worktree_dir()
|
||||
for tid in task_id:
|
||||
# Remove any matching worktree directories
|
||||
g = f'{tid}-*'
|
||||
matching_wts = wt_base.glob(g)
|
||||
for wt_dir in matching_wts:
|
||||
click.echo(f"Disposing worktree {wt_dir}")
|
||||
# unregister worktree; then delete the directory if still present
|
||||
rel = wt_dir.relative_to(root)
|
||||
subprocess.run(['git', 'worktree', 'remove', str(rel), '--force'], cwd=root)
|
||||
if wt_dir.exists():
|
||||
shutil.rmtree(wt_dir)
|
||||
else:
|
||||
print(f"No worktrees matching {g} in {wt_base}")
|
||||
# prune any stale worktree entries
|
||||
subprocess.run(['git', 'worktree', 'prune'], cwd=root)
|
||||
# Delete any matching branches
|
||||
# delete any matching local branches cleanly via for-each-ref
|
||||
ref_pattern = f'refs/heads/agentydragon-{tid}-*'
|
||||
branches = subprocess.run(
|
||||
['git', 'for-each-ref', '--format=%(refname:short)', ref_pattern],
|
||||
capture_output=True, text=True, cwd=root
|
||||
).stdout.splitlines()
|
||||
branches = [br for br in branches if br]
|
||||
if branches:
|
||||
click.echo(f"Disposing branches: {branches}")
|
||||
subprocess.run(['git', 'branch', '-D', *branches], cwd=root)
|
||||
else:
|
||||
click.echo(f"No branches matching {ref_pattern}")
|
||||
click.echo(f'Disposed task {tid}')
|
||||
# If the task was marked Done, auto-move it into .done/
|
||||
files = list(task_dir().glob(f"{tid}-*.md"))
|
||||
if len(files) == 1:
|
||||
path = files[0]
|
||||
meta, _ = load_task(path)
|
||||
if meta.status == TaskStatus.DONE:
|
||||
done_dir = task_dir() / '.done'
|
||||
done_dir.mkdir(exist_ok=True)
|
||||
target = done_dir / path.name
|
||||
click.echo(f"Moving {path.name} -> .done/ (status Done)")
|
||||
subprocess.run(['git', 'mv', str(path), str(target)], cwd=repo_root())
|
||||
|
||||
@cli.command()
|
||||
@click.argument('task_id', nargs=-1)
|
||||
def launch(task_id):
|
||||
"""Copy tmux launch one-liner for TASK_ID(s) to clipboard"""
|
||||
cmd = ['create-task-worktree.sh', '--agent', '--tmux'] + list(task_id)
|
||||
line = ' '.join(cmd)
|
||||
# system clipboard
|
||||
try:
|
||||
subprocess.run(['pbcopy'], input=line.encode(), check=True)
|
||||
click.echo('Copied to clipboard:')
|
||||
except FileNotFoundError:
|
||||
click.echo(line)
|
||||
return
|
||||
click.echo(line)
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
14
agentydragon/tools/manager_utils/launch_ready_to_merge.sh
Executable file
14
agentydragon/tools/manager_utils/launch_ready_to_merge.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# launch_ready_to_merge.sh: open tmux panes for all tasks marked Ready to merge
|
||||
set -euo pipefail
|
||||
|
||||
# Gather all tasks flagged Ready to merge by the status script
|
||||
ready=$(agentydragon_task.py status \
|
||||
| sed -n -e '1,/^Ready to merge:/d' -e 's/^Ready to merge:[ ]*//')
|
||||
if [ -z "$ready" ]; then
|
||||
echo "No tasks are Ready to merge."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Launching tasks: $ready"
|
||||
agentydragon/tools/create-task-worktree.sh --agent --tmux $ready
|
||||
28
agentydragon/tools/manager_utils/organize_done_tasks.py
Executable file
28
agentydragon/tools/manager_utils/organize_done_tasks.py
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
organize_done_tasks.py: Move merged task files under tasks/.done/ subdirectory.
|
||||
|
||||
This script should be run once to migrate all tasks with status "Merged"
|
||||
to the .done folder.
|
||||
"""
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from tasklib import task_dir, load_task
|
||||
|
||||
def main():
|
||||
root = task_dir()
|
||||
done_dir = root / '.done'
|
||||
done_dir.mkdir(exist_ok=True)
|
||||
for md in sorted(root.glob('[0-9][0-9]-*.md')):
|
||||
if md.name == 'task-template.md' or md.name.endswith('-plan.md'):
|
||||
continue
|
||||
meta, _ = load_task(md)
|
||||
if meta.status == 'Merged':
|
||||
target = done_dir / md.name
|
||||
print(f'Moving {md.name} -> .done/')
|
||||
subprocess.run(['git', 'mv', str(md), str(target)], check=True)
|
||||
print('Migration complete.')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
60
agentydragon/tools/manager_utils/tasklib.py
Normal file
60
agentydragon/tools/manager_utils/tasklib.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Simple library for loading and saving task metadata embedded as TOML front-matter
|
||||
in task Markdown files.
|
||||
"""
|
||||
import re
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import toml
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
FRONTMATTER_RE = re.compile(r"^\+\+\+\s*(.*?)\s*\+\+\+", re.S | re.M)
|
||||
|
||||
def repo_root():
|
||||
return Path(subprocess.check_output(['git', 'rev-parse', '--show-toplevel']).decode().strip())
|
||||
|
||||
def task_dir():
|
||||
return repo_root() / "agentydragon/tasks"
|
||||
|
||||
def worktree_dir():
|
||||
return task_dir() / ".worktrees"
|
||||
|
||||
class TaskStatus(str, Enum):
|
||||
NOT_STARTED = "Not started"
|
||||
IN_PROGRESS = "In progress"
|
||||
NEEDS_INPUT = "Needs input"
|
||||
NEEDS_MANUAL_REVIEW = "Needs manual review"
|
||||
DONE = "Done"
|
||||
CANCELLED = "Cancelled"
|
||||
MERGED = "Merged"
|
||||
|
||||
class TaskMeta(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
status: TaskStatus
|
||||
freeform_status: str = Field(default="")
|
||||
dependencies: str = Field(default="")
|
||||
last_updated: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
def load_task(path: Path) -> (TaskMeta, str):
|
||||
text = path.read_text(encoding='utf-8')
|
||||
m = FRONTMATTER_RE.match(text)
|
||||
if not m:
|
||||
raise ValueError(f"No TOML frontmatter in {path}")
|
||||
meta = toml.loads(m.group(1))
|
||||
tm = TaskMeta(**meta)
|
||||
body = text[m.end():].lstrip('\n')
|
||||
return tm, body
|
||||
|
||||
def save_task(path: Path, meta: TaskMeta, body: str) -> None:
|
||||
tm = meta.dict()
|
||||
# Serialize enum to its string value for front-matter
|
||||
if isinstance(tm.get('status'), Enum):
|
||||
tm['status'] = tm['status'].value
|
||||
tm['last_updated'] = meta.last_updated.isoformat()
|
||||
fm = toml.dumps(tm).strip()
|
||||
content = f"+++\n{fm}\n+++\n\n{body.lstrip()}"
|
||||
path.write_text(content, encoding='utf-8')
|
||||
3
agentydragon/tools/manager_utils/tests/__init__.py
Normal file
3
agentydragon/tools/manager_utils/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Test package for manager_utils
|
||||
"""
|
||||
35
agentydragon/tools/manager_utils/tests/test_tasklib.py
Normal file
35
agentydragon/tools/manager_utils/tests/test_tasklib.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
import toml
|
||||
import pytest
|
||||
|
||||
from ..tasklib import TaskMeta, load_task, save_task
|
||||
|
||||
SAMPLE = """+++
|
||||
id = "99"
|
||||
title = "Sample Task"
|
||||
status = "Not started"
|
||||
dependencies = ""
|
||||
last_updated = "2023-01-01T12:00:00"
|
||||
+++
|
||||
|
||||
# Body here
|
||||
"""
|
||||
|
||||
def test_load_and_save(tmp_path):
|
||||
md = tmp_path / '99-sample.md'
|
||||
md.write_text(SAMPLE)
|
||||
meta, body = load_task(md)
|
||||
assert meta.id == '99'
|
||||
assert 'Body here' in body
|
||||
meta.status = 'Done'
|
||||
save_task(md, meta, body)
|
||||
text = md.read_text()
|
||||
data = toml.loads(text.split('+++')[1])
|
||||
assert data['status'] == 'Done'
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
def test_meta_model_validation():
|
||||
with pytest.raises(ValidationError):
|
||||
TaskMeta(id='a', title='t', status='bogus', dependencies='', last_updated='bad')
|
||||
@@ -6,6 +6,7 @@ import { ReviewDecision } from "../../utils/agent/review";
|
||||
import { Select } from "../vendor/ink-select/select";
|
||||
import TextInput from "../vendor/ink-text-input";
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import { sessionScopedApprovalLabel } from "../../utils/string-utils";
|
||||
import React from "react";
|
||||
|
||||
// default deny‑reason:
|
||||
@@ -80,16 +81,23 @@ export function TerminalChatCommandReview({
|
||||
| { label: string; value: "switch" }
|
||||
> = [
|
||||
{
|
||||
label: "Yes (y)",
|
||||
label: "Yes, run this command (y)",
|
||||
value: ReviewDecision.YES,
|
||||
},
|
||||
];
|
||||
|
||||
if (showAlwaysApprove) {
|
||||
opts.push({
|
||||
label: "Yes, always approve this exact command for this session (a)",
|
||||
value: ReviewDecision.ALWAYS,
|
||||
});
|
||||
let label: string;
|
||||
if (
|
||||
React.isValidElement(confirmationPrompt) &&
|
||||
typeof (confirmationPrompt as any).props?.commandForDisplay === "string"
|
||||
) {
|
||||
const cmd: string = (confirmationPrompt as any).props.commandForDisplay;
|
||||
label = sessionScopedApprovalLabel(cmd, 30);
|
||||
} else {
|
||||
label = "Always allow this command for the remainder of the session (a)";
|
||||
}
|
||||
opts.push({ label, value: ReviewDecision.ALWAYS });
|
||||
}
|
||||
|
||||
opts.push(
|
||||
@@ -117,7 +125,7 @@ export function TerminalChatCommandReview({
|
||||
);
|
||||
|
||||
return opts;
|
||||
}, [showAlwaysApprove]);
|
||||
}, [showAlwaysApprove, confirmationPrompt]);
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
|
||||
27
codex-cli/src/utils/string-utils.ts
Normal file
27
codex-cli/src/utils/string-utils.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Truncate a string in the middle to ensure its length does not exceed maxLength.
|
||||
* If the input is longer than maxLength, replaces the middle with a single-character ellipsis '…'.
|
||||
*/
|
||||
export function truncateMiddle(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
const ellipsis = '…';
|
||||
const trimLength = maxLength - ellipsis.length;
|
||||
const startLength = Math.ceil(trimLength / 2);
|
||||
const endLength = Math.floor(trimLength / 2);
|
||||
return text.slice(0, startLength) + ellipsis + text.slice(text.length - endLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a session-scoped approval label for a given command.
|
||||
* Embeds a truncated snippet of the first line of commandForDisplay.
|
||||
*/
|
||||
export function sessionScopedApprovalLabel(
|
||||
commandForDisplay: string,
|
||||
maxLength: number,
|
||||
): string {
|
||||
const firstLine = commandForDisplay.split('\n')[0].trim();
|
||||
const snippet = truncateMiddle(firstLine, maxLength);
|
||||
return `Yes, always allow running \`${snippet}\` for this session (a)`;
|
||||
}
|
||||
39
codex-cli/tests/string-utils.test.ts
Normal file
39
codex-cli/tests/string-utils.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { truncateMiddle, sessionScopedApprovalLabel } from "../src/utils/string-utils";
|
||||
|
||||
describe("truncateMiddle", () => {
|
||||
it("returns the original string when shorter than max length", () => {
|
||||
expect(truncateMiddle("short", 10)).toBe("short");
|
||||
});
|
||||
|
||||
it("returns the original string when equal to max length", () => {
|
||||
expect(truncateMiddle("exactlen", 8)).toBe("exactlen");
|
||||
});
|
||||
|
||||
it("truncates the middle of a longer string", () => {
|
||||
const text = "abcdefghij"; // length 10
|
||||
// maxLength 7 => trimLength=6, startLen=3, endLen=3 => "abc…hij"
|
||||
expect(truncateMiddle(text, 7)).toBe("abc…hij");
|
||||
});
|
||||
|
||||
it("handles odd max lengths correctly", () => {
|
||||
const text = "abcdefghijkl"; // length 12
|
||||
// maxLength 8 => trimLength=7, startLen=4, endLen=3 => "abcd…ijk"
|
||||
expect(truncateMiddle(text, 8)).toBe("abcd…ijk");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sessionScopedApprovalLabel", () => {
|
||||
const cmd = "echo hello world";
|
||||
|
||||
it("embeds the full command when shorter than max length", () => {
|
||||
expect(sessionScopedApprovalLabel(cmd, 50)).toBe(
|
||||
"Yes, always allow running `echo hello world` for this session (a)",
|
||||
);
|
||||
});
|
||||
|
||||
it("embeds a truncated command when longer than max length", () => {
|
||||
const longCmd = "cat " + "a".repeat(100) + " end";
|
||||
const label = sessionScopedApprovalLabel(longCmd, 20);
|
||||
expect(label).toMatch(/^Yes, always allow running `.{0,20}` for this session \(a\)$/);
|
||||
});
|
||||
});
|
||||
93
codex-orchestration-plan.md
Normal file
93
codex-orchestration-plan.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Codex Orchestration Framework: Plan & Open Questions
|
||||
|
||||
This document collects the high‑level architecture, planned features, and unresolved design decisions for the proposed **codex-agents** orchestration framework.
|
||||
|
||||
## 1. Architecture & Core Components
|
||||
|
||||
- **XDG‑compliant configuration & state**
|
||||
- Repo‑local overrides: `<repo>/.codex-agent/config.toml`
|
||||
- User‑wide config: `$XDG_CONFIG_HOME/codex-agents/config.toml`
|
||||
- Global task registry: `$XDG_DATA_HOME/codex-agents/tasks.json`
|
||||
|
||||
- **CLI & optional TUI**
|
||||
- `codex-agent init` → bootstrap repo (copy prompts, create directories)
|
||||
- `codex-agent status [--tui]` → show global and per‑repo task/merge status
|
||||
- `codex-agent config` → inspect or edit effective config
|
||||
- `codex-agent agents` → view per‑agent instruction overrides
|
||||
|
||||
- **Task management (`codex-agent task`)**
|
||||
- `add`, `list`, `edit`, `worktree add|remove`, `validate`, `review`, `complete`
|
||||
- Interactive AI Q&A flow for `task add` to auto‑populate slug, goal, dependencies, and stub file
|
||||
|
||||
- **Worktree hydration**
|
||||
- OS‑aware reflink: macOS `cp -cRp`, Linux `cp --reflink=auto`, fallback to `rsync`
|
||||
- COW setup via `git worktree add --no-checkout` + hydration step
|
||||
|
||||
- **Merge & Conflict Resolver (`codex-agent merge`)**
|
||||
- `merge check` → dry‑run merge in temp worktree
|
||||
- `merge resolve` → AI‑driven conflict resolution or explicit bail-out
|
||||
- `merge rebase` → manual rebase entrypoint
|
||||
|
||||
- **Code Validator (`codex-agent task validate|review`)**
|
||||
- Run linters/tests, then invoke Validator agent prompt
|
||||
- Enforce configurable policies (doc coverage, style rules, test thresholds)
|
||||
|
||||
- **Project Manager (`codex-agent manager`)**
|
||||
- Wave planning, parallel launch commands, live monitoring of worktrees
|
||||
|
||||
## 2. Phased Roadmap
|
||||
|
||||
Phase | Deliverables
|
||||
:----:|:--------------------------------------------------------------------------------------
|
||||
1 | XDG config + global `tasks.json` + basic `task list|add|worktree` CLI
|
||||
2 | Merge check & conflict-resolver prompt + `merge check|resolve` commands
|
||||
3 | Validator agent integration + `task validate|review`
|
||||
4 | Project Manager planning & launching (`manager plan|launch|monitor`)
|
||||
5 | Interactive `task add` QA loop + per-agent instruction overrides
|
||||
6 | TUI mode for `status` + live dashboard
|
||||
7 | Polishing docs, tests, packaging, and PyPI release
|
||||
|
||||
## 3. Open Questions & Design Decisions
|
||||
|
||||
1. **Global registry schema**
|
||||
- What additional fields should `tasks.json` track? (e.g. priority, owner, labels)
|
||||
|
||||
2. **Config file format & schema**
|
||||
- TOML vs YAML vs JSON for `config.toml`?
|
||||
- Which policy keys to expose for Validator and Resolver agents?
|
||||
|
||||
3. **Per‑agent instruction overrides**
|
||||
- How to structure override files (`validator.toml`, `conflict-resolver.toml`, etc.)?
|
||||
- Should we fallback to AGENTS‑style instruction files in the repo root if present?
|
||||
|
||||
4. **CLI command names & flags**
|
||||
- Confirm subcommand verbs (`merge resolve` vs `task rebase`, `task validate` vs `task lint`)
|
||||
- Standardize flags for interactive vs non‑interactive modes
|
||||
|
||||
5. **Conflict Resolver scope**
|
||||
- Auto‑resolve only trivial hunks, or attempt full rebase‑based AI resolution?
|
||||
- How and when can the agent “give up” and hand control back to the user?
|
||||
|
||||
6. **Validator policies & auto‑fix**
|
||||
- Default policy values (max line length, doc coverage %)
|
||||
- Should `--auto-fix` let the agent rewrite code, or only report issues?
|
||||
|
||||
7. **Interactive Task Creation**
|
||||
- Best UX for prompting the user: CLI Q&A loop vs opening an editor with agent instructions?
|
||||
- How to capture dependencies and inject them into the new task stub?
|
||||
|
||||
8. **Session restore UX**
|
||||
- Always on for `codex session <UUID>`, or opt‑in via flag?
|
||||
- How to surface restore failures or drift in transcript format?
|
||||
|
||||
9. **TUI implementation**
|
||||
- Framework choice (curses, Rich, Textual)
|
||||
- Auto‑refresh interval and keybindings for actions (open worktree, resolve, validate)
|
||||
|
||||
10. **Packaging & distribution**
|
||||
- Final PyPI package name (`codex-agents` vs `ai-orchestrator`)
|
||||
- Versioning strategy and backwards‑compatibility guarantees
|
||||
|
||||
---
|
||||
|
||||
_This plan will evolve as we answer these questions and move through the roadmap phases._
|
||||
172
codex-rs/Cargo.lock
generated
172
codex-rs/Cargo.lock
generated
@@ -588,10 +588,14 @@ dependencies = [
|
||||
"codex-login",
|
||||
"codex-mcp-server",
|
||||
"codex-tui",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -766,6 +770,7 @@ dependencies = [
|
||||
"image",
|
||||
"lazy_static",
|
||||
"mcp-types",
|
||||
"notify",
|
||||
"path-clean",
|
||||
"pretty_assertions",
|
||||
"ratatui",
|
||||
@@ -773,8 +778,10 @@ dependencies = [
|
||||
"regex-lite",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"similar",
|
||||
"strum 0.27.1",
|
||||
"strum_macros 0.27.1",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
@@ -924,7 +931,7 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"crossterm_winapi",
|
||||
"mio",
|
||||
"mio 1.0.3",
|
||||
"parking_lot",
|
||||
"rustix 0.38.44",
|
||||
"signal-hook",
|
||||
@@ -1364,6 +1371,18 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
@@ -1444,6 +1463,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
@@ -2059,6 +2087,26 @@ version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.7"
|
||||
@@ -2207,6 +2255,26 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lalrpop"
|
||||
version = "0.19.12"
|
||||
@@ -2285,6 +2353,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2470,6 +2539,18 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.3"
|
||||
@@ -2568,6 +2649,25 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
@@ -3837,7 +3937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 1.0.3",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
@@ -4362,7 +4462,7 @@ dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 1.0.3",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
@@ -5083,6 +5183,15 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -5101,6 +5210,21 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
@@ -5133,6 +5257,12 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -5145,6 +5275,12 @@ version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -5157,6 +5293,12 @@ version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -5181,6 +5323,12 @@ version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -5193,6 +5341,12 @@ version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -5205,6 +5359,12 @@ version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -5217,6 +5377,12 @@ version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
|
||||
@@ -11,6 +11,18 @@ npm i -g @openai/codex@native
|
||||
codex
|
||||
```
|
||||
|
||||
You can also build and install the Rust-native binary from source:
|
||||
|
||||
```shell
|
||||
cargo install --path cli --locked
|
||||
```
|
||||
|
||||
By default this installs into `$HOME/.cargo/bin`, so make sure that's on your `PATH`. To install system-wide (e.g. to `/usr/local`), run:
|
||||
|
||||
```shell
|
||||
sudo cargo install --path cli --locked --root /usr/local
|
||||
```
|
||||
|
||||
You can also download a platform-specific release directly from our [GitHub Releases](https://github.com/openai/codex/releases).
|
||||
|
||||
## What's new in the Rust CLI
|
||||
@@ -25,12 +37,68 @@ Codex supports a rich set of configuration options. Note that the Rust CLI uses
|
||||
|
||||
Codex CLI functions as an MCP client that can connect to MCP servers on startup. See the [`mcp_servers`](./config.md#mcp_servers) section in the configuration documentation for details.
|
||||
|
||||
For example, to configure an external MCP server in your `~/.codex/config.toml`:
|
||||
|
||||
```toml
|
||||
[mcp_servers.server-name]
|
||||
command = "npx"
|
||||
args = ["-y", "mcp-server"]
|
||||
env = { "API_KEY" = "value" }
|
||||
```
|
||||
|
||||
It is still experimental, but you can also launch Codex as an MCP _server_ by running `codex mcp`. Use the [`@modelcontextprotocol/inspector`](https://github.com/modelcontextprotocol/inspector) to try it out:
|
||||
|
||||
```shell
|
||||
npx @modelcontextprotocol/inspector codex mcp
|
||||
```
|
||||
|
||||
Under the hood, `codex mcp` launches a local MCP server process that communicates over stdin/stdout
|
||||
using the Model Context Protocol (JSON-RPC messages). It reads `JSONRPCMessage` requests (e.g.
|
||||
`Initialize`, `ListTools`, `CallTool`) from stdin, handles them, and writes JSONRPCMessage
|
||||
responses and notifications to stdout. No separate container or VM is spun up and torn down; on
|
||||
Linux the process is optionally sandboxed via Landlock/seccomp (and on macOS via Seatbelt).
|
||||
See `codex-rs/mcp-server/src/lib.rs` for the implementation.
|
||||
|
||||
By default, the server advertises a single MCP tool named `codex`. A `ListTools` request
|
||||
will return this tool along with its input schema (fields: `prompt`, `model`, `profile`,
|
||||
`cwd`, `approval_policy`, `sandbox_permissions`, `config`).
|
||||
|
||||
#### Example: ListTools and CallTool messages
|
||||
|
||||
```jsonc
|
||||
// ListTools request
|
||||
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }
|
||||
|
||||
// ListTools response (abbreviated)
|
||||
{ "jsonrpc": "2.0", "id": 1,
|
||||
"result": { "tools": [
|
||||
{
|
||||
"name": "codex",
|
||||
"description": "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.",
|
||||
"input_schema": { /* JSON Schema with properties: prompt, model, profile, cwd, approval_policy, sandbox_permissions, config */ }
|
||||
}
|
||||
] }
|
||||
}
|
||||
|
||||
// CallTool request to run the 'codex' tool with a prompt
|
||||
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
|
||||
"params": { "name": "codex",
|
||||
"arguments": { "prompt": "Hello, world!" }
|
||||
}
|
||||
}
|
||||
|
||||
// CallTool response (abbreviated)
|
||||
{ "jsonrpc": "2.0", "id": 2,
|
||||
"result": {
|
||||
"content": [ { "type": "text", "text": "Hello, world! How can I help?", "annotations": null } ],
|
||||
"is_error": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Calls to the `codex` tool are handled via JSON-RPC `CallTool` requests by spawning an interactive Codex
|
||||
session based on the provided parameters and streaming the generated text in the `content` field.
|
||||
|
||||
### Notifications
|
||||
|
||||
You can enable notifications by configuring a script that is run whenever the agent finishes a turn. The [notify documentation](./config.md#notify) includes a detailed example that explains how to get desktop notifications via [terminal-notifier](https://github.com/julienXX/terminal-notifier) on macOS.
|
||||
@@ -43,6 +111,17 @@ To run Codex non-interactively, run `codex exec PROMPT` (you can also pass the p
|
||||
|
||||
Sometimes it is not convenient to `cd` to the directory you want Codex to use as the "working root" before running Codex. Fortunately, `codex` supports a `--cd` option so you can specify whatever folder you want. You can confirm that Codex is honoring `--cd` by double-checking the **workdir** it reports in the TUI at the start of a new session.
|
||||
|
||||
### `codex config` to manage your configuration file
|
||||
|
||||
Codex now provides a built-in `config` subcommand for managing your `config.toml`:
|
||||
|
||||
```shell
|
||||
codex config edit # open ~/.codex/config.toml in $EDITOR (or vi)
|
||||
codex config set KEY VALUE # set a config key to a TOML literal, e.g. tui.auto_mount_repo true
|
||||
```
|
||||
|
||||
Use `codex config --help` for more details.
|
||||
|
||||
### Experimenting with the Codex Sandbox
|
||||
|
||||
To test to see what happens when a command is run under the sandbox provided by Codex, we provide the following subcommands in Codex CLI:
|
||||
|
||||
@@ -25,6 +25,8 @@ codex-linux-sandbox = { path = "../linux-sandbox" }
|
||||
codex-mcp-server = { path = "../mcp-server" }
|
||||
codex-tui = { path = "../tui" }
|
||||
serde_json = "1"
|
||||
toml = "0.8"
|
||||
serde = "1"
|
||||
tokio = { version = "1", features = [
|
||||
"io-std",
|
||||
"macros",
|
||||
@@ -34,3 +36,7 @@ tokio = { version = "1", features = [
|
||||
] }
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
91
codex-rs/cli/src/inspect_env.rs
Normal file
91
codex-rs/cli/src/inspect_env.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use codex_common::{CliConfigOverrides, SandboxPermissionOption};
|
||||
use codex_cli::debug_sandbox::create_sandbox_policy;
|
||||
use codex_core::config::{Config, ConfigOverrides};
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
|
||||
/// Inspect the sandbox and container environment (mounts, permissions, network)
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct InspectEnvArgs {
|
||||
/// Convenience alias for low-friction sandboxed automatic execution (network-disabled sandbox that can write to cwd and TMPDIR)
|
||||
#[arg(long = "full-auto", default_value_t = false)]
|
||||
pub full_auto: bool,
|
||||
|
||||
/// Sandbox permission overrides (network, mounts)
|
||||
#[clap(flatten)]
|
||||
pub sandbox: SandboxPermissionOption,
|
||||
|
||||
#[clap(skip)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
}
|
||||
|
||||
/// Run the inspect-env command.
|
||||
pub async fn run_inspect_env(
|
||||
args: InspectEnvArgs,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
) -> anyhow::Result<()> {
|
||||
// Build sandbox policy from CLI flags.
|
||||
let sandbox_policy = create_sandbox_policy(args.full_auto, args.sandbox);
|
||||
// Load configuration to include any -c overrides and sandbox policy.
|
||||
let config = Config::load_with_cli_overrides(
|
||||
args.config_overrides.parse_overrides().map_err(anyhow::Error::msg)?,
|
||||
ConfigOverrides {
|
||||
sandbox_policy: Some(sandbox_policy.clone()),
|
||||
codex_linux_sandbox_exe,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
let policy = &config.sandbox_policy;
|
||||
let cwd = &config.cwd;
|
||||
|
||||
// Compute mount entries: root and writable roots.
|
||||
let mut mounts = Vec::new();
|
||||
if policy.has_full_disk_write_access() {
|
||||
mounts.push(("/".to_string(), "rw".to_string()));
|
||||
} else if policy.has_full_disk_read_access() {
|
||||
mounts.push(("/".to_string(), "ro".to_string()));
|
||||
}
|
||||
let writable_roots = policy.get_writable_roots_with_cwd(cwd);
|
||||
for root in writable_roots.iter() {
|
||||
let path = root.display().to_string();
|
||||
if path != "/" {
|
||||
mounts.push((path, "rw".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine column width for PATH.
|
||||
let width = mounts.iter().map(|(p, _)| p.len()).max().unwrap_or(0).max(4);
|
||||
|
||||
// Header.
|
||||
println!("Sandbox & Container Environment\n");
|
||||
|
||||
// Mounts.
|
||||
println!("Mounts:");
|
||||
println!(" {:<width$} {}", "PATH", "MODE", width = width);
|
||||
println!(" {:-<width$} {:-<4}", "", "", width = width);
|
||||
for (path, mode) in &mounts {
|
||||
println!(" {:<width$} {}", path, mode, width = width);
|
||||
}
|
||||
println!();
|
||||
|
||||
// Permissions.
|
||||
println!("Permissions:");
|
||||
for perm in policy.permissions() {
|
||||
println!(" - {:?}", perm);
|
||||
}
|
||||
println!();
|
||||
|
||||
// Network status.
|
||||
let net = if policy.has_full_network_access() { "enabled" } else { "disabled" };
|
||||
println!("Network: {}", net);
|
||||
println!();
|
||||
|
||||
// Summary.
|
||||
println!("Summary:");
|
||||
println!(" Mount count: {}", mounts.len());
|
||||
println!(" Writable roots: {}", writable_roots.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,9 +4,15 @@ use codex_cli::SeatbeltCommand;
|
||||
use codex_cli::login::run_login_with_chatgpt;
|
||||
use codex_cli::proto;
|
||||
use codex_common::CliConfigOverrides;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_exec::Cli as ExecCli;
|
||||
use codex_tui::Cli as TuiCli;
|
||||
use serde::de::Error as SerdeError;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs, process};
|
||||
use toml::{self, Value, value::Table};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::proto::ProtoCli;
|
||||
|
||||
@@ -31,8 +37,41 @@ struct MultitoolCli {
|
||||
subcommand: Option<Subcommand>,
|
||||
}
|
||||
|
||||
/// Parse a raw TOML literal (e.g. `true`, `42`, `[1,2]`, `{a=1}`) into a TOML Value.
|
||||
/// Wraps the literal under a sentinel key to satisfy the TOML parser.
|
||||
fn parse_toml_value(raw: &str) -> Result<Value, toml::de::Error> {
|
||||
let wrapped = format!("_x_ = {raw}");
|
||||
let table: Table = toml::from_str(&wrapped)?;
|
||||
table
|
||||
.get("_x_")
|
||||
.cloned()
|
||||
.ok_or_else(|| SerdeError::custom("missing sentinel"))
|
||||
}
|
||||
|
||||
/// Subcommands for the `codex config` command.
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
enum ConfigCmd {
|
||||
/// Open the config file in your editor ($EDITOR or vi).
|
||||
Edit,
|
||||
/// Set a configuration key to a TOML literal, e.g. `tui.auto_mount_repo true`.
|
||||
Set {
|
||||
/// Dotted path to the key (e.g. `tui.auto_mount_repo`).
|
||||
key: String,
|
||||
/// A TOML literal value (e.g. `true`, `42`, `"foo"`, `[1,2]`).
|
||||
value: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
enum Subcommand {
|
||||
/// Resume an existing TUI session by UUID.
|
||||
Session {
|
||||
/// UUID of the session to resume
|
||||
session_id: Uuid,
|
||||
},
|
||||
/// Inspect or modify the CLI configuration file.
|
||||
#[command(subcommand)]
|
||||
Config(ConfigCmd),
|
||||
/// Run Codex non-interactively.
|
||||
#[clap(visible_alias = "e")]
|
||||
Exec(ExecCli),
|
||||
@@ -88,6 +127,50 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
||||
prepend_config_flags(&mut tui_cli.config_overrides, cli.config_overrides);
|
||||
codex_tui::run_main(tui_cli, codex_linux_sandbox_exe)?;
|
||||
}
|
||||
Some(Subcommand::Session { session_id }) => {
|
||||
let mut tui_cli = cli.interactive;
|
||||
tui_cli.session = Some(session_id);
|
||||
prepend_config_flags(&mut tui_cli.config_overrides, cli.config_overrides);
|
||||
codex_tui::run_main(tui_cli, codex_linux_sandbox_exe)?;
|
||||
}
|
||||
Some(Subcommand::Config(cmd)) => {
|
||||
// Handle `codex config` subcommands: edit or set.
|
||||
// Determine config directory and file path.
|
||||
let codex_home = find_codex_home()?;
|
||||
fs::create_dir_all(&codex_home)?;
|
||||
let config_path = codex_home.join("config.toml");
|
||||
// Load existing config.toml into a Toml value, or start with empty table.
|
||||
let mut doc = match fs::read_to_string(&config_path) {
|
||||
Ok(s) => toml::from_str::<toml::Value>(&s)?,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => toml::Value::Table(Default::default()),
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
match cmd {
|
||||
ConfigCmd::Edit => {
|
||||
// Ensure the config file exists.
|
||||
if !config_path.exists() {
|
||||
fs::write(&config_path, "")?;
|
||||
}
|
||||
// Open in editor from $EDITOR or fall back to vi.
|
||||
let editor = env::var_os("EDITOR").unwrap_or_else(|| "vi".into());
|
||||
let status = process::Command::new(editor).arg(&config_path).status()?;
|
||||
if !status.success() {
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
}
|
||||
ConfigCmd::Set { key, value } => {
|
||||
// Parse the provided TOML literal value.
|
||||
let val = parse_toml_value(&value)
|
||||
.map_err(|e| anyhow::anyhow!("TOML parse error for `{}`: {}", value, e))?;
|
||||
// Apply the override into the document.
|
||||
apply_override(&mut doc, &key, val);
|
||||
// Serialize and write back to disk.
|
||||
let s = toml::to_string_pretty(&doc)?;
|
||||
fs::write(&config_path, s)?;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Some(Subcommand::Exec(mut exec_cli)) => {
|
||||
prepend_config_flags(&mut exec_cli.config_overrides, cli.config_overrides);
|
||||
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
|
||||
@@ -136,3 +219,63 @@ fn prepend_config_flags(
|
||||
.raw_overrides
|
||||
.splice(0..0, cli_config_overrides.raw_overrides);
|
||||
}
|
||||
|
||||
/// Apply a dotted-path override into a TOML document, creating tables as needed.
|
||||
fn apply_override(root: &mut toml::Value, path: &str, value: toml::Value) {
|
||||
use toml::value::Table;
|
||||
let parts: Vec<&str> = path.split('.').collect();
|
||||
let mut current = root;
|
||||
for (i, part) in parts.iter().enumerate() {
|
||||
let last = i == parts.len() - 1;
|
||||
if last {
|
||||
match current {
|
||||
toml::Value::Table(tbl) => {
|
||||
tbl.insert((*part).to_string(), value);
|
||||
}
|
||||
_ => {
|
||||
let mut tbl = Table::new();
|
||||
tbl.insert((*part).to_string(), value);
|
||||
*current = toml::Value::Table(tbl);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
match current {
|
||||
toml::Value::Table(tbl) => {
|
||||
current = tbl
|
||||
.entry((*part).to_string())
|
||||
.or_insert_with(|| toml::Value::Table(Table::new()));
|
||||
}
|
||||
_ => {
|
||||
*current = toml::Value::Table(Table::new());
|
||||
if let toml::Value::Table(tbl) = current {
|
||||
current = tbl
|
||||
.entry((*part).to_string())
|
||||
.or_insert_with(|| toml::Value::Table(Table::new()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// Tests for CLI parsing
|
||||
// ---------------------
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::MultitoolCli;
|
||||
use clap::CommandFactory;
|
||||
|
||||
#[test]
|
||||
fn config_subcommands_help() {
|
||||
let mut cmd = MultitoolCli::command();
|
||||
let cfg = cmd
|
||||
.find_subcommand_mut("config")
|
||||
.expect("config subcommand not found");
|
||||
let mut buf = Vec::new();
|
||||
cfg.write_long_help(&mut buf).unwrap();
|
||||
let help = String::from_utf8(buf).unwrap();
|
||||
assert!(help.contains("edit"), "help missing 'edit': {}", help);
|
||||
assert!(help.contains("set"), "help missing 'set': {}", help);
|
||||
}
|
||||
}
|
||||
|
||||
43
codex-rs/cli/tests/config_cmd.rs
Normal file
43
codex-rs/cli/tests/config_cmd.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
/// Integration test for the `codex config` subcommand.
|
||||
/// This uses `CARGO_BIN_EXE_codex` to locate the compiled binary.
|
||||
#[cfg(test)]
|
||||
mod cli_config {
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use tempfile;
|
||||
use toml;
|
||||
|
||||
#[test]
|
||||
fn config_subcommand_help() {
|
||||
let exe = env!("CARGO_BIN_EXE_codex");
|
||||
let output = Command::new(exe)
|
||||
.arg("config")
|
||||
.arg("--help")
|
||||
.output()
|
||||
.expect("failed to run codex config --help");
|
||||
assert!(output.status.success(), "Exited with {:?}", output.status);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
// Should show config subcommands help
|
||||
assert!(stdout.contains("edit"), "help missing 'edit': {}", stdout);
|
||||
assert!(stdout.contains("set"), "help missing 'set': {}", stdout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_set_and_read() {
|
||||
let exe = env!("CARGO_BIN_EXE_codex");
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let cfg_path = tmp.path().join("config.toml");
|
||||
let status = Command::new(exe)
|
||||
.env("CODEX_HOME", tmp.path())
|
||||
.arg("config")
|
||||
.arg("set")
|
||||
.arg("tui.auto_mount_repo")
|
||||
.arg("true")
|
||||
.status()
|
||||
.expect("failed to run codex config set");
|
||||
assert!(status.success());
|
||||
let contents = fs::read_to_string(&cfg_path).expect("read config");
|
||||
let doc: toml::Value = toml::from_str(&contents).expect("parse config.toml");
|
||||
assert_eq!(doc["tui"]["auto_mount_repo"].as_bool(), Some(true));
|
||||
}
|
||||
}
|
||||
@@ -195,6 +195,23 @@ sandbox_permissions = [
|
||||
]
|
||||
```
|
||||
|
||||
## auto_allow
|
||||
|
||||
User-defined predicate scripts that vote on each shell command before manual approval.
|
||||
Each script is invoked with the full candidate command as its only argument and must
|
||||
write exactly one of `allow`, `deny`, or `no-opinion` to stdout.
|
||||
|
||||
```toml
|
||||
[[auto_allow]]
|
||||
script = "/path/to/approve_predicate.sh"
|
||||
[[auto_allow]]
|
||||
script = "my_predicate --flag"
|
||||
```
|
||||
|
||||
If any predicate returns `deny`, Codex rejects the command; otherwise if any returns
|
||||
`allow`, Codex auto-approves and proceeds under the sandbox; if all return `no-opinion`
|
||||
or error, Codex falls back to the manual approval prompt.
|
||||
|
||||
## mcp_servers
|
||||
|
||||
Defines the list of MCP servers that Codex can consult for tool use. Currently, only servers that are launched by executing a program that communicate over stdio are supported. For servers that use the SSE transport, consider an adapter like [mcp-proxy](https://github.com/sparfenyuk/mcp-proxy).
|
||||
@@ -396,20 +413,42 @@ hide_agent_reasoning = true # defaults to false
|
||||
|
||||
Maximum number of bytes to read from an `AGENTS.md` file to include in the instructions sent with the first turn of a session. Defaults to 32 KiB.
|
||||
|
||||
## base_instructions_override
|
||||
|
||||
The built-in system prompt (from `prompt.md`) can be overridden or disabled via environment variables:
|
||||
|
||||
`CODEX_BASE_INSTRUCTIONS_FILE`: If unset, the built-in prompt (`prompt.md`) is used.
|
||||
If set to a valid file path, that file's contents will be used instead (failure to read will abort).
|
||||
If set to an empty string or `-`, no system prompt will be sent.
|
||||
|
||||
## tui
|
||||
|
||||
Options that are specific to the TUI.
|
||||
|
||||
```toml
|
||||
[tui]
|
||||
# This will make it so that Codex does not try to process mouse events, which
|
||||
# means your Terminal's native drag-to-text to text selection and copy/paste
|
||||
# should work. The tradeoff is that Codex will not receive any mouse events, so
|
||||
# it will not be possible to use the mouse to scroll conversation history.
|
||||
#
|
||||
# Note that most terminals support holding down a modifier key when using the
|
||||
# mouse to support text selection. For example, even if Codex mouse capture is
|
||||
# enabled (i.e., this is set to `false`), you can still hold down alt while
|
||||
# dragging the mouse to select text.
|
||||
|
||||
# This will make it so that Codex does not process mouse events, which
|
||||
# means your terminal's native drag-to-text selection and copy/paste will work.
|
||||
# The tradeoff is that Codex will not receive any mouse events, so it will not
|
||||
# be possible to use the mouse to scroll conversation history.
|
||||
# Note that most terminals support a modifier key to enable text selection
|
||||
# even when mouse capture is enabled (e.g., holding Alt in iTerm).
|
||||
disable_mouse_capture = true # defaults to `false`
|
||||
|
||||
# Maximum number of visible lines in the chat input composer before scrolling.
|
||||
# The composer will expand up to this many lines; additional content will enable
|
||||
# an internal scrollbar.
|
||||
composer_max_rows = 10 # defaults to `10`
|
||||
|
||||
# Command used to launch an external editor for composing the chat prompt.
|
||||
# Defaults to `$VISUAL`, then `$EDITOR`, falling back to `nvim`.
|
||||
|
||||
editor = "${VISUAL:-${EDITOR:-nvim}}"
|
||||
|
||||
# Insert a blank line between messages for visual separation.
|
||||
message_spacing = false # defaults to `false`
|
||||
|
||||
# Render the sender label on its own line above the message content.
|
||||
sender_break_line = false # defaults to `false`
|
||||
```
|
||||
|
||||
@@ -2,6 +2,38 @@
|
||||
|
||||
This crate implements the business logic for Codex. It is designed to be used by the various Codex UIs written in Rust.
|
||||
|
||||
## System Prompt Composition
|
||||
|
||||
Codex composes the initial system message that seeds every chat completion turn as follows:
|
||||
|
||||
1. Load the built-in system prompt from `prompt.md` (unless overridden/disabled).
|
||||
2. If the `CODEX_BASE_INSTRUCTIONS_FILE` env var is set, use that file instead of `prompt.md`.
|
||||
3. Append any user instructions (e.g. from `instructions.md` and merged `AGENTS.md`).
|
||||
4. Append the apply-patch tool instructions when using GPT-4.1 models.
|
||||
5. Finally, the user's command or prompt is sent as the first user message.
|
||||
|
||||
This “system” prompt is delivered to the OpenAI Chat Completions API as the very first message with role `system` in the JSON `messages` array, e.g.:
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4.1",
|
||||
"messages": [
|
||||
{"role": "system", "content": "<base instructions here>"},
|
||||
{"role": "system", "content": "<user_instructions here>"},
|
||||
{"role": "system", "content": "<apply-patch tool instructions>"},
|
||||
{"role": "user", "content": "<your prompt>"}
|
||||
],
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The base instructions behavior can be customized with `CODEX_BASE_INSTRUCTIONS_FILE`:
|
||||
|
||||
- If unset, the built-in prompt (`prompt.md`) is used.
|
||||
- If set to a valid file path, that file's contents will be used instead (failure to read will abort).
|
||||
- If set to an empty string or `-`, no system prompt will be sent.
|
||||
|
||||
For implementation details, see `client_common.rs` and `project_doc.rs`.
|
||||
|
||||
Though for non-Rust UIs, we are also working to define a _protocol_ for talking to Codex. See:
|
||||
|
||||
- [Specification](../docs/protocol_v1.md)
|
||||
|
||||
13
codex-rs/core/init.md
Normal file
13
codex-rs/core/init.md
Normal file
@@ -0,0 +1,13 @@
|
||||
Please analyze this codebase and create a AGENTS.md file containing:
|
||||
1. Build/lint/test commands - especially for running a single test
|
||||
2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
|
||||
|
||||
Usage notes:
|
||||
- The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
|
||||
- If there's already an AGENTS.md, improve it.
|
||||
- If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
|
||||
- Be sure to prefix the file with the following text:
|
||||
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to OpenAI Codex (openai.com/codex) when working with code in this repository.
|
||||
@@ -35,22 +35,32 @@ pub(crate) async fn stream_chat_completions(
|
||||
client: &reqwest::Client,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> Result<ResponseStream> {
|
||||
// Build messages array
|
||||
// Build messages array, buffering user turns that arrive mid-tool invocation
|
||||
let mut messages = Vec::<serde_json::Value>::new();
|
||||
let mut pending_call: Option<String> = None;
|
||||
let mut buf_user: Vec<serde_json::Value> = Vec::new();
|
||||
|
||||
let full_instructions = prompt.get_full_instructions(model);
|
||||
messages.push(json!({"role": "system", "content": full_instructions}));
|
||||
|
||||
for item in &prompt.input {
|
||||
match item {
|
||||
ResponseItem::Message { role, content } if role == "user" && pending_call.is_some() => {
|
||||
// Buffer user message until pending tool result arrives
|
||||
let mut text = String::new();
|
||||
for c in content {
|
||||
if let ContentItem::InputText { text: t } = c {
|
||||
text.push_str(t);
|
||||
}
|
||||
}
|
||||
buf_user.push(json!({"role": "user", "content": text}));
|
||||
}
|
||||
ResponseItem::Message { role, content } => {
|
||||
let mut text = String::new();
|
||||
for c in content {
|
||||
match c {
|
||||
ContentItem::InputText { text: t }
|
||||
| ContentItem::OutputText { text: t } => {
|
||||
text.push_str(t);
|
||||
}
|
||||
| ContentItem::OutputText { text: t } => text.push_str(t),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -61,31 +71,30 @@ pub(crate) async fn stream_chat_completions(
|
||||
arguments,
|
||||
call_id,
|
||||
} => {
|
||||
// Mark tool invocation in-flight
|
||||
pending_call = Some(call_id.clone());
|
||||
messages.push(json!({
|
||||
"role": "assistant",
|
||||
"content": null,
|
||||
"tool_calls": [{
|
||||
"id": call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name,
|
||||
"arguments": arguments,
|
||||
}
|
||||
"function": { "name": name, "arguments": arguments }
|
||||
}]
|
||||
}));
|
||||
}
|
||||
ResponseItem::LocalShellCall {
|
||||
id,
|
||||
call_id: _,
|
||||
status,
|
||||
action,
|
||||
id, status, action, ..
|
||||
} => {
|
||||
// Confirm with API team.
|
||||
// Mark shell-call invocation in-flight by id
|
||||
if let Some(call_id) = id {
|
||||
pending_call = Some(call_id.clone());
|
||||
}
|
||||
messages.push(json!({
|
||||
"role": "assistant",
|
||||
"content": null,
|
||||
"tool_calls": [{
|
||||
"id": id.clone().unwrap_or_else(|| "".to_string()),
|
||||
"id": id.clone().unwrap_or_default(),
|
||||
"type": "local_shell_call",
|
||||
"status": status,
|
||||
"action": action,
|
||||
@@ -98,14 +107,33 @@ pub(crate) async fn stream_chat_completions(
|
||||
"tool_call_id": call_id,
|
||||
"content": output.content,
|
||||
}));
|
||||
if pending_call.as_ref() == Some(call_id) {
|
||||
// Flush any buffered user messages now that tool result arrived
|
||||
pending_call = None;
|
||||
for msg in buf_user.drain(..) {
|
||||
messages.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
ResponseItem::Reasoning { .. } | ResponseItem::Other => {
|
||||
// Omit these items from the conversation history.
|
||||
// Skip these items in the conversation history.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a tool invocation was never resolved (e.g. was cancelled), insert a fake cancellation result and flush buffered user inputs
|
||||
if let Some(call_id) = pending_call.take() {
|
||||
messages.push(json!({
|
||||
"role": "tool",
|
||||
"tool_call_id": call_id,
|
||||
"content": "Tool cancelled"
|
||||
}));
|
||||
for msg in buf_user.drain(..) {
|
||||
messages.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
let tools_json = create_tools_json_for_chat_completions_api(prompt, model)?;
|
||||
let payload = json!({
|
||||
"model": model,
|
||||
|
||||
@@ -36,13 +36,38 @@ pub struct Prompt {
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
/// Assemble the system prompt sent to the model, in order:
|
||||
/// 1. Base instructions (built-in prompt.md), unless disabled via
|
||||
/// the CODEX_DISABLE_BASE_INSTRUCTIONS env var.
|
||||
/// 2. Or, if CODEX_BASE_INSTRUCTIONS_FILE is set, load that file instead of the built-in prompt.
|
||||
/// 3. User instructions (e.g. from instructions.md and AGENTS.md), if any.
|
||||
/// 4. Apply-patch tool instructions when using GPT-4.1 models.
|
||||
pub(crate) fn get_full_instructions(&self, model: &str) -> Cow<str> {
|
||||
let mut sections: Vec<&str> = vec![BASE_INSTRUCTIONS];
|
||||
// Determine base instructions or override/disable via CODEX_BASE_INSTRUCTIONS_FILE
|
||||
let mut sections = Vec::new();
|
||||
match std::env::var("CODEX_BASE_INSTRUCTIONS_FILE") {
|
||||
Ok(ref path) if !path.is_empty() && path != "-" => {
|
||||
// Override built-in prompt: read file or abort
|
||||
let contents = std::fs::read_to_string(path).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"failed to read CODEX_BASE_INSTRUCTIONS_FILE '{}': {e}",
|
||||
path
|
||||
)
|
||||
});
|
||||
sections.push(contents);
|
||||
}
|
||||
Ok(_) => {
|
||||
// Explicitly disabled (empty or "-"): skip base instructions
|
||||
}
|
||||
Err(_) => {
|
||||
sections.push(BASE_INSTRUCTIONS.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(ref user) = self.user_instructions {
|
||||
sections.push(user);
|
||||
sections.push(user.clone());
|
||||
}
|
||||
if model.starts_with("gpt-4.1") {
|
||||
sections.push(APPLY_PATCH_TOOL_INSTRUCTIONS);
|
||||
sections.push(APPLY_PATCH_TOOL_INSTRUCTIONS.to_string());
|
||||
}
|
||||
Cow::Owned(sections.join("\n"))
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ use crate::WireApi;
|
||||
use crate::client::ModelClient;
|
||||
use crate::client_common::Prompt;
|
||||
use crate::client_common::ResponseEvent;
|
||||
use crate::config::Config;
|
||||
use crate::config::{AutoAllowPredicate, Config};
|
||||
use crate::config_types::ShellEnvironmentPolicy;
|
||||
use crate::conversation_history::ConversationHistory;
|
||||
use crate::error::CodexErr;
|
||||
@@ -83,8 +83,10 @@ use crate::protocol::Submission;
|
||||
use crate::protocol::TaskCompleteEvent;
|
||||
use crate::rollout::RolloutRecorder;
|
||||
use crate::safety::SafetyCheck;
|
||||
use crate::safety::assess_command_safety;
|
||||
use crate::safety::assess_patch_safety;
|
||||
use crate::safety::{
|
||||
AutoAllowVote, assess_command_safety, evaluate_auto_allow_predicates, get_platform_sandbox,
|
||||
};
|
||||
use crate::user_notification::UserNotification;
|
||||
use crate::util::backoff;
|
||||
|
||||
@@ -175,6 +177,8 @@ pub(crate) struct Session {
|
||||
cwd: PathBuf,
|
||||
instructions: Option<String>,
|
||||
approval_policy: AskForApproval,
|
||||
/// External predicate scripts for auto-approval or rejection of shell commands.
|
||||
pub auto_allow: Vec<AutoAllowPredicate>,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
shell_environment_policy: ShellEnvironmentPolicy,
|
||||
writable_roots: Mutex<Vec<PathBuf>>,
|
||||
@@ -658,6 +662,7 @@ async fn submission_loop(
|
||||
ctrl_c: Arc::clone(&ctrl_c),
|
||||
instructions,
|
||||
approval_policy,
|
||||
auto_allow: config.auto_allow.clone(),
|
||||
sandbox_policy,
|
||||
shell_environment_policy: config.shell_environment_policy.clone(),
|
||||
cwd,
|
||||
@@ -1278,15 +1283,34 @@ async fn handle_container_exec_with_params(
|
||||
MaybeApplyPatchVerified::NotApplyPatch => (),
|
||||
}
|
||||
|
||||
// safety checks
|
||||
let safety = {
|
||||
let state = sess.state.lock().unwrap();
|
||||
assess_command_safety(
|
||||
¶ms.command,
|
||||
sess.approval_policy,
|
||||
&sess.sandbox_policy,
|
||||
&state.approved_commands,
|
||||
)
|
||||
// safety checks with auto-approval predicates
|
||||
let safety = match evaluate_auto_allow_predicates(¶ms.command, &sess.auto_allow) {
|
||||
AutoAllowVote::Deny => {
|
||||
return ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output: crate::models::FunctionCallOutputPayload {
|
||||
content: "exec command denied by auto-approval predicate".to_string(),
|
||||
success: None,
|
||||
},
|
||||
};
|
||||
}
|
||||
AutoAllowVote::Allow => {
|
||||
let sandbox_type = if sess.sandbox_policy.is_unrestricted() {
|
||||
SandboxType::None
|
||||
} else {
|
||||
get_platform_sandbox().unwrap_or(SandboxType::None)
|
||||
};
|
||||
SafetyCheck::AutoApprove { sandbox_type }
|
||||
}
|
||||
AutoAllowVote::NoOpinion => {
|
||||
let state = sess.state.lock().unwrap();
|
||||
assess_command_safety(
|
||||
¶ms.command,
|
||||
sess.approval_policy,
|
||||
&sess.sandbox_policy,
|
||||
&state.approved_commands,
|
||||
)
|
||||
}
|
||||
};
|
||||
let sandbox_type = match safety {
|
||||
SafetyCheck::AutoApprove { sandbox_type } => sandbox_type,
|
||||
|
||||
@@ -25,6 +25,13 @@ use toml::Value as TomlValue;
|
||||
/// the context window.
|
||||
pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB
|
||||
|
||||
/// Predicate for auto-approval: external script that examines a shell command and votes.
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct AutoAllowPredicate {
|
||||
/// Command line to invoke, receiving the candidate shell command as its only argument.
|
||||
pub script: String,
|
||||
}
|
||||
|
||||
/// Application configuration loaded from disk and merged with overrides.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
@@ -39,6 +46,8 @@ pub struct Config {
|
||||
|
||||
/// Approval policy for executing commands.
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Auto-approval predicate scripts that cast votes on each shell command.
|
||||
pub auto_allow: Vec<AutoAllowPredicate>,
|
||||
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
|
||||
@@ -238,6 +247,10 @@ pub struct ConfigToml {
|
||||
/// Default approval policy for executing commands.
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
|
||||
/// Auto-approval predicate scripts that cast votes on each shell command.
|
||||
#[serde(default)]
|
||||
pub auto_allow: Vec<AutoAllowPredicate>,
|
||||
|
||||
#[serde(default)]
|
||||
pub shell_environment_policy: ShellEnvironmentPolicyToml,
|
||||
|
||||
@@ -439,6 +452,7 @@ impl Config {
|
||||
.or(config_profile.approval_policy)
|
||||
.or(cfg.approval_policy)
|
||||
.unwrap_or_else(AskForApproval::default),
|
||||
auto_allow: config_profile.auto_allow.unwrap_or(cfg.auto_allow),
|
||||
sandbox_policy,
|
||||
shell_environment_policy,
|
||||
disable_response_storage: config_profile
|
||||
@@ -493,7 +507,7 @@ fn default_model() -> String {
|
||||
/// function will Err if the path does not exist.
|
||||
/// - If `CODEX_HOME` is not set, this function does not verify that the
|
||||
/// directory exists.
|
||||
fn find_codex_home() -> std::io::Result<PathBuf> {
|
||||
pub fn find_codex_home() -> std::io::Result<PathBuf> {
|
||||
// Honor the `CODEX_HOME` environment variable when it is set to allow users
|
||||
// (and tests) to override the default location.
|
||||
if let Ok(val) = std::env::var("CODEX_HOME") {
|
||||
@@ -786,6 +800,7 @@ disable_response_storage = true
|
||||
model_provider_id: "openai".to_string(),
|
||||
model_provider: fixture.openai_provider.clone(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
auto_allow: Vec::new(),
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
disable_response_storage: false,
|
||||
@@ -828,6 +843,7 @@ disable_response_storage = true
|
||||
model_provider_id: "openai-chat-completions".to_string(),
|
||||
model_provider: fixture.openai_chat_completions_provider.clone(),
|
||||
approval_policy: AskForApproval::UnlessAllowListed,
|
||||
auto_allow: Vec::new(),
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
disable_response_storage: false,
|
||||
@@ -885,6 +901,7 @@ disable_response_storage = true
|
||||
model_provider_id: "openai".to_string(),
|
||||
model_provider: fixture.openai_provider.clone(),
|
||||
approval_policy: AskForApproval::OnFailure,
|
||||
auto_allow: Vec::new(),
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
disable_response_storage: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::AutoAllowPredicate;
|
||||
use crate::protocol::AskForApproval;
|
||||
|
||||
/// Collection of common configuration options that a user can define as a unit
|
||||
@@ -12,4 +13,6 @@ pub struct ConfigProfile {
|
||||
pub model_provider: Option<String>,
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
pub disable_response_storage: Option<bool>,
|
||||
/// External predicate scripts for auto-approval or rejection of shell commands.
|
||||
pub auto_allow: Option<Vec<AutoAllowPredicate>>,
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ pub enum HistoryPersistence {
|
||||
}
|
||||
|
||||
/// Collection of settings that are specific to the TUI.
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct Tui {
|
||||
/// By default, mouse capture is enabled in the TUI so that it is possible
|
||||
/// to scroll the conversation history with a mouse. This comes at the cost
|
||||
@@ -87,7 +87,72 @@ pub struct Tui {
|
||||
/// the mouse is not possible, though the keyboard shortcuts e.g. `b` and
|
||||
/// `space` still work. This allows the user to select text in the TUI
|
||||
/// using the mouse without needing to hold down a modifier key.
|
||||
#[serde(default)]
|
||||
pub disable_mouse_capture: bool,
|
||||
|
||||
/// When `true`, omit blank lines immediately following Markdown headings
|
||||
/// (levels 1–6) in TUI rendering for more compact vertical spacing.
|
||||
#[serde(default)]
|
||||
pub markdown_compact: bool,
|
||||
/// When true, collapse the header and first line of chat/prompt/patch events into one line
|
||||
#[serde(default)]
|
||||
pub header_compact: bool,
|
||||
|
||||
/// When `true`, insert a blank line between messages for visual separation.
|
||||
#[serde(default)]
|
||||
pub message_spacing: bool,
|
||||
|
||||
/// When `true`, render the sender label on its own line above the message content.
|
||||
#[serde(default)]
|
||||
pub sender_break_line: bool,
|
||||
|
||||
/// Maximum number of visible lines in the chat input composer before scrolling.
|
||||
/// The composer will expand up to this many lines; additional content will enable
|
||||
/// an internal scrollbar.
|
||||
#[serde(default = "default_composer_max_rows")]
|
||||
pub composer_max_rows: usize,
|
||||
/// Command used to launch the external editor for editing the chat prompt.
|
||||
/// Defaults to the `VISUAL` or `EDITOR` environment variable, falling back to `nvim`.
|
||||
#[serde(default = "default_editor")]
|
||||
pub editor: String,
|
||||
/// Require two consecutive Ctrl+D keystrokes to exit the TUI when enabled.
|
||||
#[serde(default)]
|
||||
pub require_double_ctrl_d: bool,
|
||||
/// Timeout in seconds for requiring second Ctrl+D to confirm exit.
|
||||
#[serde(default = "default_double_ctrl_d_timeout_secs")]
|
||||
pub double_ctrl_d_timeout_secs: u64,
|
||||
}
|
||||
|
||||
fn default_composer_max_rows() -> usize {
|
||||
10
|
||||
}
|
||||
|
||||
/// Default editor: `$VISUAL`, then `$EDITOR`, falling back to `nvim`.
|
||||
fn default_editor() -> String {
|
||||
std::env::var("VISUAL")
|
||||
.or_else(|_| std::env::var("EDITOR"))
|
||||
.unwrap_or_else(|_| "nvim".into())
|
||||
}
|
||||
|
||||
/// Default timeout in seconds for the second Ctrl+D confirmation to exit the TUI.
|
||||
fn default_double_ctrl_d_timeout_secs() -> u64 {
|
||||
2
|
||||
}
|
||||
|
||||
impl Default for Tui {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
disable_mouse_capture: Default::default(),
|
||||
markdown_compact: Default::default(),
|
||||
header_compact: Default::default(),
|
||||
message_spacing: Default::default(),
|
||||
sender_break_line: Default::default(),
|
||||
composer_max_rows: default_composer_max_rows(),
|
||||
editor: default_editor(),
|
||||
require_double_ctrl_d: false,
|
||||
double_ctrl_d_timeout_secs: default_double_ctrl_d_timeout_secs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
|
||||
@@ -27,6 +27,9 @@ mod model_provider_info;
|
||||
pub use model_provider_info::ModelProviderInfo;
|
||||
pub use model_provider_info::WireApi;
|
||||
mod models;
|
||||
pub use models::{
|
||||
ContentItem, FunctionCallOutputPayload, ReasoningItemReasoningSummary, ResponseItem,
|
||||
};
|
||||
pub mod openai_api_key;
|
||||
mod openai_tools;
|
||||
mod project_doc;
|
||||
@@ -36,4 +39,4 @@ mod safety;
|
||||
mod user_notification;
|
||||
pub mod util;
|
||||
|
||||
pub use client_common::model_supports_reasoning_summaries;
|
||||
pub use client_common::{Prompt, model_supports_reasoning_summaries};
|
||||
|
||||
@@ -180,7 +180,6 @@ pub struct ShellToolCallParams {
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FunctionCallOutputPayload {
|
||||
pub content: String,
|
||||
#[expect(dead_code)]
|
||||
pub success: Option<bool>,
|
||||
}
|
||||
|
||||
|
||||
@@ -253,6 +253,26 @@ impl SandboxPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
impl SandboxPolicy {
|
||||
/// Grant disk-write permission for the specified folder.
|
||||
pub fn allow_disk_write_folder(&mut self, folder: std::path::PathBuf) {
|
||||
self.permissions
|
||||
.push(SandboxPermission::DiskWriteFolder { folder });
|
||||
}
|
||||
|
||||
/// Revoke any disk-write permission for the specified folder.
|
||||
pub fn revoke_disk_write_folder<P: AsRef<std::path::Path>>(&mut self, folder: P) {
|
||||
let target = folder.as_ref();
|
||||
self.permissions.retain(|perm| {
|
||||
if let SandboxPermission::DiskWriteFolder { folder: f } = perm {
|
||||
f != target
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Permissions that should be granted to the sandbox in which the agent
|
||||
/// operates.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::path::PathBuf;
|
||||
use codex_apply_patch::ApplyPatchAction;
|
||||
use codex_apply_patch::ApplyPatchFileChange;
|
||||
|
||||
use crate::config::AutoAllowPredicate;
|
||||
use crate::exec::SandboxType;
|
||||
use crate::is_safe_command::is_known_safe_command;
|
||||
use crate::protocol::AskForApproval;
|
||||
@@ -113,6 +114,55 @@ pub fn get_platform_sandbox() -> Option<SandboxType> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Vote returned by auto-approval predicate scripts.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AutoAllowVote {
|
||||
/// Script approved the command.
|
||||
Allow,
|
||||
/// Script denied the command.
|
||||
Deny,
|
||||
/// Script had no opinion (or errored).
|
||||
NoOpinion,
|
||||
}
|
||||
|
||||
/// Evaluate user-configured auto-approval predicates for the given command.
|
||||
/// Invokes each script in order, passing the full candidate command as the only argument.
|
||||
/// Returns the first `Allow` or `Deny` vote, or `NoOpinion` if none asserted.
|
||||
pub fn evaluate_auto_allow_predicates(
|
||||
command: &[String],
|
||||
predicates: &[AutoAllowPredicate],
|
||||
) -> AutoAllowVote {
|
||||
if predicates.is_empty() {
|
||||
return AutoAllowVote::NoOpinion;
|
||||
}
|
||||
let cmd_text = command.join(" ");
|
||||
for pred in predicates {
|
||||
let output = std::process::Command::new(&pred.script)
|
||||
.arg(&cmd_text)
|
||||
.output();
|
||||
let vote = match output {
|
||||
Ok(output) if output.status.success() => match String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
"allow" => AutoAllowVote::Allow,
|
||||
"deny" => AutoAllowVote::Deny,
|
||||
"no-opinion" => AutoAllowVote::NoOpinion,
|
||||
_ => AutoAllowVote::NoOpinion,
|
||||
},
|
||||
_ => AutoAllowVote::NoOpinion,
|
||||
};
|
||||
if vote == AutoAllowVote::Deny {
|
||||
return AutoAllowVote::Deny;
|
||||
}
|
||||
if vote == AutoAllowVote::Allow {
|
||||
return AutoAllowVote::Allow;
|
||||
}
|
||||
}
|
||||
AutoAllowVote::NoOpinion
|
||||
}
|
||||
|
||||
fn is_write_patch_constrained_to_writable_paths(
|
||||
action: &ApplyPatchAction,
|
||||
writable_roots: &[PathBuf],
|
||||
@@ -192,6 +242,10 @@ mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
use crate::config::AutoAllowPredicate;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_writable_roots_constraint() {
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
@@ -224,4 +278,112 @@ mod tests {
|
||||
&cwd,
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_auto_allow_predicates_votes() {
|
||||
let dir = tempdir().unwrap();
|
||||
let allow_script = dir.path().join("allow.sh");
|
||||
std::fs::write(&allow_script, "#!/usr/bin/env bash\necho allow\n").unwrap();
|
||||
let mut perms = std::fs::metadata(&allow_script).unwrap().permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&allow_script, perms).unwrap();
|
||||
|
||||
let deny_script = dir.path().join("deny.sh");
|
||||
std::fs::write(&deny_script, "#!/usr/bin/env bash\necho deny\n").unwrap();
|
||||
let mut perms2 = std::fs::metadata(&deny_script).unwrap().permissions();
|
||||
perms2.set_mode(0o755);
|
||||
std::fs::set_permissions(&deny_script, perms2).unwrap();
|
||||
|
||||
// Allow script should return Allow
|
||||
let preds = vec![AutoAllowPredicate {
|
||||
script: allow_script.to_string_lossy().into(),
|
||||
}];
|
||||
let vote = evaluate_auto_allow_predicates(&["cmd".to_string()], &preds);
|
||||
assert_eq!(vote, AutoAllowVote::Allow);
|
||||
|
||||
// Deny script takes precedence over allow
|
||||
let preds2 = vec![
|
||||
AutoAllowPredicate {
|
||||
script: deny_script.to_string_lossy().into(),
|
||||
},
|
||||
AutoAllowPredicate {
|
||||
script: allow_script.to_string_lossy().into(),
|
||||
},
|
||||
];
|
||||
let vote2 = evaluate_auto_allow_predicates(&["cmd".to_string()], &preds2);
|
||||
assert_eq!(vote2, AutoAllowVote::Deny);
|
||||
|
||||
// No predicates yields NoOpinion
|
||||
let vote3 = evaluate_auto_allow_predicates(&["cmd".to_string()], &[]);
|
||||
assert_eq!(vote3, AutoAllowVote::NoOpinion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_auto_allow_predicates_various_no_opinion_cases() {
|
||||
let dir = tempdir().unwrap();
|
||||
// Script that explicitly returns no-opinion
|
||||
let noop_script = dir.path().join("noop.sh");
|
||||
std::fs::write(&noop_script, "#!/usr/bin/env bash\necho no-opinion\n").unwrap();
|
||||
let mut perms = std::fs::metadata(&noop_script).unwrap().permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&noop_script, perms).unwrap();
|
||||
|
||||
// Script that returns unknown output
|
||||
let unknown_script = dir.path().join("unknown.sh");
|
||||
std::fs::write(&unknown_script, "#!/usr/bin/env bash\necho maybe\n").unwrap();
|
||||
let mut perms2 = std::fs::metadata(&unknown_script).unwrap().permissions();
|
||||
perms2.set_mode(0o755);
|
||||
std::fs::set_permissions(&unknown_script, perms2).unwrap();
|
||||
|
||||
// Script that exits with an error
|
||||
let error_script = dir.path().join("error.sh");
|
||||
std::fs::write(&error_script, "#!/usr/bin/env bash\nexit 1\n").unwrap();
|
||||
let mut perms3 = std::fs::metadata(&error_script).unwrap().permissions();
|
||||
perms3.set_mode(0o755);
|
||||
std::fs::set_permissions(&error_script, perms3).unwrap();
|
||||
|
||||
// All scripts no-opinion or error yields NoOpinion
|
||||
let preds = vec![
|
||||
AutoAllowPredicate {
|
||||
script: noop_script.to_string_lossy().into(),
|
||||
},
|
||||
AutoAllowPredicate {
|
||||
script: unknown_script.to_string_lossy().into(),
|
||||
},
|
||||
AutoAllowPredicate {
|
||||
script: error_script.to_string_lossy().into(),
|
||||
},
|
||||
];
|
||||
let vote = evaluate_auto_allow_predicates(&["cmd".to_string()], &preds);
|
||||
assert_eq!(vote, AutoAllowVote::NoOpinion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_auto_allow_predicates_short_circuits_after_no_opinion() {
|
||||
let dir = tempdir().unwrap();
|
||||
// First script no-opinion
|
||||
let noop_script = dir.path().join("noop2.sh");
|
||||
std::fs::write(&noop_script, "#!/usr/bin/env bash\necho no-opinion\n").unwrap();
|
||||
let mut perms = std::fs::metadata(&noop_script).unwrap().permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&noop_script, perms).unwrap();
|
||||
|
||||
// Second script allow
|
||||
let allow_script = dir.path().join("allow2.sh");
|
||||
std::fs::write(&allow_script, "#!/usr/bin/env bash\necho allow\n").unwrap();
|
||||
let mut perms2 = std::fs::metadata(&allow_script).unwrap().permissions();
|
||||
perms2.set_mode(0o755);
|
||||
std::fs::set_permissions(&allow_script, perms2).unwrap();
|
||||
|
||||
let preds = vec![
|
||||
AutoAllowPredicate {
|
||||
script: noop_script.to_string_lossy().into(),
|
||||
},
|
||||
AutoAllowPredicate {
|
||||
script: allow_script.to_string_lossy().into(),
|
||||
},
|
||||
];
|
||||
let vote = evaluate_auto_allow_predicates(&["cmd".to_string()], &preds);
|
||||
assert_eq!(vote, AutoAllowVote::Allow);
|
||||
}
|
||||
}
|
||||
|
||||
151
codex-rs/core/tests/guard_tool_output_sequencing.rs
Normal file
151
codex-rs/core/tests/guard_tool_output_sequencing.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use codex_core::{ContentItem, FunctionCallOutputPayload, ResponseItem};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// Reproduce the `messages` JSON construction from `stream_chat_completions`
|
||||
fn build_messages(input: Vec<ResponseItem>, _model: &str) -> Vec<Value> {
|
||||
let mut messages = Vec::new();
|
||||
let mut pending = None::<String>;
|
||||
let mut buf_user = Vec::new();
|
||||
|
||||
// placeholder system instructions (not under test)
|
||||
messages.push(json!({"role": "system", "content": ""}));
|
||||
|
||||
for item in input {
|
||||
match item {
|
||||
ResponseItem::Message { role, content } if role == "user" && pending.is_some() => {
|
||||
let mut text = String::new();
|
||||
for c in content {
|
||||
if let ContentItem::InputText { text: t } = c {
|
||||
text.push_str(&t);
|
||||
}
|
||||
}
|
||||
buf_user.push(json!({"role": "user", "content": text}));
|
||||
}
|
||||
ResponseItem::Message { role, content } => {
|
||||
let mut text = String::new();
|
||||
for c in content {
|
||||
if let ContentItem::InputText { text: t } = c {
|
||||
text.push_str(&t);
|
||||
}
|
||||
}
|
||||
messages.push(json!({"role": role, "content": text}));
|
||||
}
|
||||
ResponseItem::FunctionCall {
|
||||
name,
|
||||
arguments,
|
||||
call_id,
|
||||
} => {
|
||||
pending = Some(call_id.clone());
|
||||
messages.push(json!({
|
||||
"role": "assistant", "content": null,
|
||||
"tool_calls": [{"id": call_id, "type": "function", "function": {"name": name, "arguments": arguments}}]
|
||||
}));
|
||||
}
|
||||
ResponseItem::FunctionCallOutput { call_id, output } => {
|
||||
messages.push(
|
||||
json!({"role": "tool", "tool_call_id": call_id, "content": output.content}),
|
||||
);
|
||||
if pending.as_ref() == Some(&call_id) {
|
||||
pending = None;
|
||||
for m in buf_user.drain(..) {
|
||||
messages.push(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// cancellation: no output arrived
|
||||
if let Some(call_id) = pending {
|
||||
messages
|
||||
.push(json!({"role": "tool", "tool_call_id": call_id, "content": "Tool cancelled"}));
|
||||
for m in buf_user.drain(..) {
|
||||
messages.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
messages
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_flow_no_buffer() {
|
||||
let input = vec![ResponseItem::Message {
|
||||
role: "user".into(),
|
||||
content: vec![ContentItem::InputText { text: "hi".into() }],
|
||||
}];
|
||||
let msgs = build_messages(input, "m1");
|
||||
assert_eq!(
|
||||
msgs.iter().filter(|m| m["role"] == json!("user")).count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_and_flush_on_output() {
|
||||
let call_id = "c1".to_string();
|
||||
let input = vec![
|
||||
ResponseItem::FunctionCall {
|
||||
name: "f".into(),
|
||||
arguments: "{}".into(),
|
||||
call_id: call_id.clone(),
|
||||
},
|
||||
ResponseItem::Message {
|
||||
role: "user".into(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "late".into(),
|
||||
}],
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: call_id.clone(),
|
||||
output: FunctionCallOutputPayload {
|
||||
content: "ok".into(),
|
||||
success: None,
|
||||
},
|
||||
},
|
||||
];
|
||||
let msgs = build_messages(input, "m1");
|
||||
// order: system, functioncall, tool output, then buffered user
|
||||
let roles: Vec<_> = msgs.iter().map(|m| m["role"].clone()).collect();
|
||||
assert_eq!(
|
||||
roles.as_slice(),
|
||||
&[
|
||||
json!("system"),
|
||||
json!("assistant"),
|
||||
json!("tool"),
|
||||
json!("user")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_and_cancel() {
|
||||
let call_id = "c2".to_string();
|
||||
let input = vec![
|
||||
ResponseItem::FunctionCall {
|
||||
name: "f".into(),
|
||||
arguments: "{}".into(),
|
||||
call_id: call_id.clone(),
|
||||
},
|
||||
ResponseItem::Message {
|
||||
role: "user".into(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "oops".into(),
|
||||
}],
|
||||
},
|
||||
];
|
||||
let msgs = build_messages(input, "m1");
|
||||
// expect system, functioncall, fake cancel, then user
|
||||
let roles: Vec<_> = msgs.iter().map(|m| m["role"].clone()).collect();
|
||||
assert_eq!(
|
||||
roles.as_slice(),
|
||||
&[
|
||||
json!("system"),
|
||||
json!("assistant"),
|
||||
json!("tool"),
|
||||
json!("user")
|
||||
]
|
||||
);
|
||||
// cancellation message content
|
||||
assert_eq!(msgs[2]["content"], json!("Tool cancelled"));
|
||||
}
|
||||
@@ -174,7 +174,12 @@ impl EventProcessor {
|
||||
ts_println!(self, "{prefix} {message}");
|
||||
}
|
||||
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
|
||||
ts_println!(self, "{}", message.style(self.dimmed));
|
||||
// Collapse verbose sandbox-denied and retry logs into exec flow; skip them here.
|
||||
if message.contains("sandbox denied") || message.contains("retrying") {
|
||||
// drop verbose background messages for exec errors/retries
|
||||
} else {
|
||||
ts_println!(self, "{}", message.style(self.dimmed));
|
||||
}
|
||||
}
|
||||
EventMsg::TaskStarted | EventMsg::TaskComplete(_) => {
|
||||
// Ignore.
|
||||
@@ -470,6 +475,10 @@ impl EventProcessor {
|
||||
}
|
||||
|
||||
fn escape_command(command: &[String]) -> String {
|
||||
// Strip any `bash -lc` wrapper and return the inner command directly.
|
||||
if command.len() == 3 && command[0] == "bash" && command[1] == "-lc" {
|
||||
return command[2].clone();
|
||||
}
|
||||
try_join(command.iter().map(|s| s.as_str())).unwrap_or_else(|_| command.join(" "))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ use clap::Parser;
|
||||
use codex_common::SandboxPermissionOption;
|
||||
use std::ffi::CString;
|
||||
|
||||
use libc;
|
||||
|
||||
use crate::landlock::apply_sandbox_policy_to_current_thread;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -22,11 +24,10 @@ pub fn run_main() -> ! {
|
||||
None => codex_core::protocol::SandboxPolicy::new_read_only_policy(),
|
||||
};
|
||||
|
||||
let cwd = match std::env::current_dir() {
|
||||
// Determine working directory inside the session.
|
||||
let mut cwd = match std::env::current_dir() {
|
||||
Ok(cwd) => cwd,
|
||||
Err(e) => {
|
||||
panic!("failed to getcwd(): {e:?}");
|
||||
}
|
||||
Err(e) => panic!("failed to getcwd(): {e:?}"),
|
||||
};
|
||||
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(&sandbox_policy, &cwd) {
|
||||
|
||||
@@ -39,6 +39,7 @@ serde_json = { version = "1", features = ["preserve_order"] }
|
||||
shlex = "1.3.0"
|
||||
strum = "0.27.1"
|
||||
strum_macros = "0.27.1"
|
||||
tempfile = "3"
|
||||
tokio = { version = "1", features = [
|
||||
"io-std",
|
||||
"macros",
|
||||
@@ -54,6 +55,8 @@ tui-markdown = "0.3.3"
|
||||
tui-textarea = "0.7.0"
|
||||
unicode-segmentation = "1.12.0"
|
||||
uuid = "1"
|
||||
notify = "6"
|
||||
similar = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::chatwidget::ChatWidget;
|
||||
use crate::confirm_ctrl_d::ConfirmCtrlD;
|
||||
use crate::git_warning_screen::GitWarningOutcome;
|
||||
use crate::git_warning_screen::GitWarningScreen;
|
||||
use crate::login_screen::LoginScreen;
|
||||
@@ -8,9 +9,8 @@ use crate::mouse_capture::MouseCapture;
|
||||
use crate::scroll_event_helper::ScrollEventHelper;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::tui;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::protocol::Event;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::config::{Config, ConfigOverrides};
|
||||
use codex_core::protocol::{Event, EventMsg, Op, SessionConfiguredEvent};
|
||||
use color_eyre::eyre::Result;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
@@ -19,6 +19,14 @@ use crossterm::event::MouseEventKind;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::time::Instant;
|
||||
|
||||
use codex_core::ResponseItem;
|
||||
use uuid::Uuid;
|
||||
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::thread;
|
||||
|
||||
/// Top-level application state: which full-screen view is currently active.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -46,6 +54,9 @@ pub(crate) struct App<'a> {
|
||||
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
|
||||
/// after dismissing the Git-repo warning.
|
||||
chat_args: Option<ChatWidgetArgs>,
|
||||
session_id: Option<Uuid>,
|
||||
/// Tracks Ctrl+D confirmation state when enabled in config.
|
||||
confirm_ctrl_d: ConfirmCtrlD,
|
||||
}
|
||||
|
||||
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
|
||||
@@ -57,6 +68,101 @@ struct ChatWidgetArgs {
|
||||
initial_images: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Parse raw argument string for `/mount-add host=... container=... mode=...`.
|
||||
fn parse_mount_add_args(
|
||||
raw: &str,
|
||||
) -> Result<(std::path::PathBuf, std::path::PathBuf, String), String> {
|
||||
let mut host = None;
|
||||
let mut container = None;
|
||||
let mut mode = "rw".to_string();
|
||||
for token in raw.split_whitespace() {
|
||||
let mut parts = token.splitn(2, '=');
|
||||
let key = parts.next().unwrap();
|
||||
let value = parts
|
||||
.next()
|
||||
.ok_or_else(|| format!("invalid argument '{}'", token))?;
|
||||
match key {
|
||||
"host" => host = Some(std::path::PathBuf::from(value)),
|
||||
"container" => container = Some(std::path::PathBuf::from(value)),
|
||||
"mode" => mode = value.to_string(),
|
||||
_ => return Err(format!("unknown argument '{}'", key)),
|
||||
}
|
||||
}
|
||||
let host = host.ok_or_else(|| "missing 'host' argument".to_string())?;
|
||||
let container = container.ok_or_else(|| "missing 'container' argument".to_string())?;
|
||||
Ok((host, container, mode))
|
||||
}
|
||||
|
||||
/// Parse raw argument string for `/mount-remove container=...`.
|
||||
fn parse_mount_remove_args(raw: &str) -> Result<std::path::PathBuf, String> {
|
||||
let mut container = None;
|
||||
for token in raw.split_whitespace() {
|
||||
let mut parts = token.splitn(2, '=');
|
||||
let key = parts.next().unwrap();
|
||||
let value = parts
|
||||
.next()
|
||||
.ok_or_else(|| format!("invalid argument '{}'", token))?;
|
||||
if key == "container" {
|
||||
container = Some(std::path::PathBuf::from(value));
|
||||
} else {
|
||||
return Err(format!("unknown argument '{}'", key));
|
||||
}
|
||||
}
|
||||
container.ok_or_else(|| "missing 'container' argument".to_string())
|
||||
}
|
||||
|
||||
/// Handle inline mount-add DSL event.
|
||||
fn handle_inline_mount_add(config: &mut Config, raw: &str) -> Result<(), String> {
|
||||
let (host, container, mode) = parse_mount_add_args(raw)?;
|
||||
do_mount_add(config, &host, &container, &mode).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Handle inline mount-remove DSL event.
|
||||
fn handle_inline_mount_remove(config: &mut Config, raw: &str) -> Result<(), String> {
|
||||
let container = parse_mount_remove_args(raw)?;
|
||||
do_mount_remove(config, &container).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Perform mount-add: create symlink under cwd and update sandbox policy.
|
||||
fn do_mount_add(
|
||||
config: &mut Config,
|
||||
host: &std::path::PathBuf,
|
||||
container: &std::path::PathBuf,
|
||||
mode: &str,
|
||||
) -> std::io::Result<()> {
|
||||
let host_abs = std::fs::canonicalize(host)?;
|
||||
let target = config.cwd.join(container);
|
||||
if target.exists() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::AlreadyExists,
|
||||
format!("target '{}' already exists", target.display()),
|
||||
));
|
||||
}
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::symlink(&host_abs, &target)?;
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if host_abs.is_file() {
|
||||
std::os::windows::fs::symlink_file(&host_abs, &target)?;
|
||||
} else {
|
||||
std::os::windows::fs::symlink_dir(&host_abs, &target)?;
|
||||
}
|
||||
}
|
||||
if mode.contains('w') {
|
||||
config.sandbox_policy.allow_disk_write_folder(host_abs);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform mount-remove: remove symlink under cwd and revoke sandbox policy.
|
||||
fn do_mount_remove(config: &mut Config, container: &std::path::PathBuf) -> std::io::Result<()> {
|
||||
let target = config.cwd.join(container);
|
||||
let host = std::fs::read_link(&target)?;
|
||||
std::fs::remove_file(&target)?;
|
||||
config.sandbox_policy.revoke_disk_write_folder(host);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
pub(crate) fn new(
|
||||
config: Config,
|
||||
@@ -160,8 +266,13 @@ impl<'a> App<'a> {
|
||||
app_event_tx,
|
||||
app_event_rx,
|
||||
app_state,
|
||||
config,
|
||||
config: config.clone(),
|
||||
chat_args,
|
||||
session_id: None,
|
||||
confirm_ctrl_d: ConfirmCtrlD::new(
|
||||
config.tui.require_double_ctrl_d,
|
||||
config.tui.double_ctrl_d_timeout_secs,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +282,25 @@ impl<'a> App<'a> {
|
||||
self.app_event_tx.clone()
|
||||
}
|
||||
|
||||
/// Override the session ID for this UI instance (useful for session-resume).
|
||||
pub fn set_session_id(&mut self, id: Uuid) {
|
||||
self.session_id = Some(id);
|
||||
}
|
||||
|
||||
/// Replay a previous session transcript into the chat widget.
|
||||
pub fn replay_items(&mut self, items: Vec<ResponseItem>) {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.replay_items(items);
|
||||
}
|
||||
}
|
||||
|
||||
/// Override the session ID for this UI instance (useful for session-resume).
|
||||
|
||||
/// Returns the session ID assigned by the backend for this session, if available.
|
||||
pub fn session_id(&self) -> Option<Uuid> {
|
||||
self.session_id
|
||||
}
|
||||
|
||||
pub(crate) fn run(
|
||||
&mut self,
|
||||
terminal: &mut tui::Tui,
|
||||
@@ -181,10 +311,93 @@ impl<'a> App<'a> {
|
||||
app_event_tx.send(AppEvent::Redraw);
|
||||
|
||||
while let Ok(event) = self.app_event_rx.recv() {
|
||||
// Expire pending Ctrl+D confirmation and clear any prompt overlay.
|
||||
let now = Instant::now();
|
||||
self.confirm_ctrl_d.expire(now);
|
||||
if self.config.tui.require_double_ctrl_d && !self.confirm_ctrl_d.is_confirming() {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.clear_exit_confirmation_prompt();
|
||||
}
|
||||
}
|
||||
match event {
|
||||
AppEvent::Redraw => {
|
||||
self.draw_next_frame(terminal)?;
|
||||
}
|
||||
AppEvent::InlineMountAdd(args) => {
|
||||
if let Err(err) = handle_inline_mount_add(&mut self.config, &args) {
|
||||
tracing::error!("mount-add failed: {err}");
|
||||
}
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
AppEvent::InlineMountRemove(args) => {
|
||||
if let Err(err) = handle_inline_mount_remove(&mut self.config, &args) {
|
||||
tracing::error!("mount-remove failed: {err}");
|
||||
}
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
AppEvent::InlineInspectEnv(_raw) => {
|
||||
let tx = self.app_event_tx.clone();
|
||||
thread::spawn(move || {
|
||||
match Command::new("codex")
|
||||
.arg("inspect-env")
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(mut child) => {
|
||||
if let Some(stdout) = child.stdout.take() {
|
||||
let reader = BufReader::new(stdout);
|
||||
for line in reader.lines().flatten() {
|
||||
let _ = tx.send(AppEvent::LatestLog(line));
|
||||
}
|
||||
}
|
||||
let _ = child.wait();
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = tx.send(AppEvent::LatestLog(format!(
|
||||
"Failed to spawn inspect-env: {err}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
let _ = tx.send(AppEvent::Redraw);
|
||||
});
|
||||
}
|
||||
AppEvent::MountAdd {
|
||||
host,
|
||||
container,
|
||||
mode,
|
||||
} => {
|
||||
if let Err(err) = do_mount_add(&mut self.config, &host, &container, &mode) {
|
||||
tracing::error!("mount-add failed: {err}");
|
||||
}
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
AppEvent::MountRemove { container } => {
|
||||
if let Err(err) = do_mount_remove(&mut self.config, &container) {
|
||||
tracing::error!("mount-remove failed: {err}");
|
||||
}
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
AppEvent::ConfigReloadRequest(diff) => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.push_config_reload(diff);
|
||||
}
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
AppEvent::ConfigReloadApply => {
|
||||
match Config::load_with_cli_overrides(Vec::new(), ConfigOverrides::default()) {
|
||||
Ok(new_cfg) => {
|
||||
self.config = new_cfg.clone();
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.update_config(new_cfg);
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::error!("Failed to reload config.toml: {e}"),
|
||||
}
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
AppEvent::ConfigReloadIgnore => {
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
AppEvent::KeyEvent(key_event) => {
|
||||
match key_event {
|
||||
KeyEvent {
|
||||
@@ -207,7 +420,16 @@ impl<'a> App<'a> {
|
||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||
..
|
||||
} => {
|
||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||
// Handle Ctrl+D exit confirmation when enabled.
|
||||
let now = Instant::now();
|
||||
if self.confirm_ctrl_d.handle(now) {
|
||||
break;
|
||||
}
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.show_exit_confirmation_prompt(
|
||||
"Press Ctrl+D again to confirm exit".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.dispatch_key_event(key_event);
|
||||
@@ -247,10 +469,57 @@ impl<'a> App<'a> {
|
||||
tracing::error!("Failed to toggle mouse mode: {e}");
|
||||
}
|
||||
}
|
||||
SlashCommand::EditPrompt => {
|
||||
// External-editor prompt handled inline by the composer; no-op here.
|
||||
}
|
||||
SlashCommand::Quit => {
|
||||
break;
|
||||
}
|
||||
SlashCommand::MountAdd => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.push_mount_add_interactive();
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
}
|
||||
SlashCommand::MountRemove => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.push_mount_remove_interactive();
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
}
|
||||
SlashCommand::InspectEnv => {
|
||||
// Activate inspect-env view and initiate output streaming
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.push_inspect_env();
|
||||
}
|
||||
let _ = self
|
||||
.app_event_tx
|
||||
.send(AppEvent::InlineInspectEnv(String::new()));
|
||||
}
|
||||
SlashCommand::Shell => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.push_shell_command_interactive();
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
}
|
||||
},
|
||||
AppEvent::ShellCommand(cmd) => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_shell_command(cmd);
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
}
|
||||
AppEvent::ShellCommandResult {
|
||||
call_id,
|
||||
stdout,
|
||||
stderr,
|
||||
exit_code,
|
||||
} => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_shell_command_result(call_id, stdout, stderr, exit_code);
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
terminal.clear()?;
|
||||
@@ -316,6 +585,10 @@ impl<'a> App<'a> {
|
||||
}
|
||||
|
||||
fn dispatch_codex_event(&mut self, event: Event) {
|
||||
// Capture session ID when the session is initially configured
|
||||
if let EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, .. }) = &event.msg {
|
||||
self.session_id = Some(*session_id);
|
||||
}
|
||||
match &mut self.app_state {
|
||||
AppState::Chat { widget } => widget.handle_codex_event(event),
|
||||
AppState::Login { .. } | AppState::GitWarning { .. } => {}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crossterm::event::KeyEvent;
|
||||
use crate::slash_command::SlashCommand;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AppEvent {
|
||||
CodexEvent(Event),
|
||||
|
||||
@@ -27,5 +28,89 @@ pub(crate) enum AppEvent {
|
||||
|
||||
/// Dispatch a recognized slash command from the UI (composer) to the app
|
||||
/// layer so it can be handled centrally.
|
||||
/// Dispatch a recognized slash command from the UI (composer) to the app
|
||||
/// layer so it can be handled centrally (interactive dialog).
|
||||
DispatchCommand(SlashCommand),
|
||||
/// Inline mount-add DSL: raw argument string (`host=... container=... mode=...`).
|
||||
InlineMountAdd(String),
|
||||
/// Inline mount-remove DSL: raw argument string (`container=...`).
|
||||
InlineMountRemove(String),
|
||||
/// Inline inspect-env DSL: raw argument string (unused).
|
||||
InlineInspectEnv(String),
|
||||
/// Perform mount-add: create symlink and update sandbox policy.
|
||||
MountAdd {
|
||||
host: std::path::PathBuf,
|
||||
container: std::path::PathBuf,
|
||||
mode: String,
|
||||
},
|
||||
/// Perform mount-remove: remove symlink and update sandbox policy.
|
||||
MountRemove {
|
||||
container: std::path::PathBuf,
|
||||
},
|
||||
/// Notify that the on-disk config.toml has changed and present diff.
|
||||
ConfigReloadRequest(String),
|
||||
/// Apply the new on-disk config.toml.
|
||||
ConfigReloadApply,
|
||||
/// Ignore on-disk config.toml changes and continue with old config.
|
||||
ConfigReloadIgnore,
|
||||
/// Run an arbitrary shell command in the agent's container (from hotkey prompt).
|
||||
ShellCommand(String),
|
||||
/// Result of a previously-invoked shell command: call ID, stdout, stderr, and exit code.
|
||||
ShellCommandResult {
|
||||
call_id: String,
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
exit_code: i32,
|
||||
},
|
||||
}
|
||||
|
||||
impl PartialEq for AppEvent {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
use AppEvent::*;
|
||||
match (self, other) {
|
||||
(CodexEvent(_), CodexEvent(_)) => true,
|
||||
(Redraw, Redraw) => true,
|
||||
(KeyEvent(a), KeyEvent(b)) => a == b,
|
||||
(Scroll(a), Scroll(b)) => a == b,
|
||||
(ExitRequest, ExitRequest) => true,
|
||||
(CodexOp(a), CodexOp(b)) => a == b,
|
||||
(LatestLog(a), LatestLog(b)) => a == b,
|
||||
(DispatchCommand(a), DispatchCommand(b)) => a == b,
|
||||
(InlineMountAdd(a), InlineMountAdd(b)) => a == b,
|
||||
(InlineMountRemove(a), InlineMountRemove(b)) => a == b,
|
||||
(InlineInspectEnv(a), InlineInspectEnv(b)) => a == b,
|
||||
(
|
||||
MountAdd {
|
||||
host: h1,
|
||||
container: c1,
|
||||
mode: m1,
|
||||
},
|
||||
MountAdd {
|
||||
host: h2,
|
||||
container: c2,
|
||||
mode: m2,
|
||||
},
|
||||
) => h1 == h2 && c1 == c2 && m1 == m2,
|
||||
(MountRemove { container: c1 }, MountRemove { container: c2 }) => c1 == c2,
|
||||
(ConfigReloadRequest(a), ConfigReloadRequest(b)) => a == b,
|
||||
(ConfigReloadApply, ConfigReloadApply) => true,
|
||||
(ConfigReloadIgnore, ConfigReloadIgnore) => true,
|
||||
(ShellCommand(a), ShellCommand(b)) => a == b,
|
||||
(
|
||||
ShellCommandResult {
|
||||
call_id: i1,
|
||||
stdout: o1,
|
||||
stderr: e1,
|
||||
exit_code: x1,
|
||||
},
|
||||
ShellCommandResult {
|
||||
call_id: i2,
|
||||
stdout: o2,
|
||||
stderr: e2,
|
||||
exit_code: x2,
|
||||
},
|
||||
) => i1 == i2 && o1 == o2 && e1 == e2 && x1 == x2,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ use crossterm::event::KeyEvent;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Alignment;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::style::{Color, Style, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::BorderType;
|
||||
use ratatui::widgets::Borders;
|
||||
@@ -18,6 +17,7 @@ use super::command_popup::CommandPopup;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::slash_command::SlashCommand;
|
||||
|
||||
/// Minimum number of visible text rows inside the textarea.
|
||||
const MIN_TEXTAREA_ROWS: usize = 1;
|
||||
@@ -25,6 +25,7 @@ const MIN_TEXTAREA_ROWS: usize = 1;
|
||||
const BORDER_LINES: u16 = 2;
|
||||
|
||||
/// Result returned when the user interacts with the text area.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum InputResult {
|
||||
Submitted(String),
|
||||
None,
|
||||
@@ -35,10 +36,47 @@ pub(crate) struct ChatComposer<'a> {
|
||||
command_popup: Option<CommandPopup>,
|
||||
app_event_tx: AppEventSender,
|
||||
history: ChatComposerHistory,
|
||||
/// Maximum number of visible lines in the chat input composer.
|
||||
max_rows: usize,
|
||||
/// Last computed context-left percentage
|
||||
context_left_percent: f64,
|
||||
/// Whether the composer is in shell-command mode (Ctrl+M toggles).
|
||||
shell_mode: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::sync::mpsc;
|
||||
|
||||
#[test]
|
||||
fn ctrl_m_dispatches_shell_command() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let evt_tx = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(true, evt_tx.clone(), 1);
|
||||
// Initial shell_mode should be false.
|
||||
assert!(!composer.shell_mode);
|
||||
// Simulate Ctrl+M key event.
|
||||
let key_event = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::CONTROL);
|
||||
let (res, needs_redraw) = composer.handle_key_event(key_event);
|
||||
assert!(needs_redraw);
|
||||
assert_eq!(res, InputResult::None);
|
||||
// shell_mode should have toggled to true.
|
||||
assert!(composer.shell_mode);
|
||||
// Verify DispatchCommand(Shell) event was sent.
|
||||
match rx.recv().unwrap() {
|
||||
AppEvent::DispatchCommand(cmd) => assert_eq!(cmd, SlashCommand::Shell),
|
||||
other => panic!("Expected DispatchCommand(Shell), got {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatComposer<'_> {
|
||||
pub fn new(has_input_focus: bool, app_event_tx: AppEventSender) -> Self {
|
||||
pub fn new(has_input_focus: bool, app_event_tx: AppEventSender, max_rows: usize) -> Self {
|
||||
let mut textarea = TextArea::default();
|
||||
textarea.set_placeholder_text("send a message");
|
||||
textarea.set_cursor_line_style(ratatui::style::Style::default());
|
||||
@@ -48,6 +86,9 @@ impl ChatComposer<'_> {
|
||||
command_popup: None,
|
||||
app_event_tx,
|
||||
history: ChatComposerHistory::new(),
|
||||
max_rows,
|
||||
context_left_percent: 100.0,
|
||||
shell_mode: false,
|
||||
};
|
||||
this.update_border(has_input_focus);
|
||||
this
|
||||
@@ -76,6 +117,11 @@ impl ChatComposer<'_> {
|
||||
self.update_border(has_focus);
|
||||
}
|
||||
|
||||
/// Update the context-left percentage for display.
|
||||
pub fn set_context_left(&mut self, pct: f64) {
|
||||
self.context_left_percent = pct;
|
||||
}
|
||||
|
||||
/// Handle a key event coming from the main UI.
|
||||
pub fn handle_key_event(&mut self, key_event: KeyEvent) -> (InputResult, bool) {
|
||||
let result = match self.command_popup {
|
||||
@@ -133,14 +179,40 @@ impl ChatComposer<'_> {
|
||||
ctrl: false,
|
||||
} => {
|
||||
if let Some(cmd) = popup.selected_command() {
|
||||
// Send command to the app layer.
|
||||
self.app_event_tx.send(AppEvent::DispatchCommand(*cmd));
|
||||
|
||||
// Clear textarea so no residual text remains.
|
||||
// Inline DSL for mount-add/remove with args or dispatch other commands.
|
||||
let first_line = self
|
||||
.textarea
|
||||
.lines()
|
||||
.first()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("");
|
||||
let stripped = first_line
|
||||
.trim_start()
|
||||
.strip_prefix('/')
|
||||
.unwrap_or(first_line);
|
||||
let mut parts = stripped.splitn(2, char::is_whitespace);
|
||||
let _cmd_token = parts.next().unwrap_or("");
|
||||
let args = parts.next().unwrap_or("").trim_start();
|
||||
// Launch external editor for prompt drafting when slash command is /edit-prompt
|
||||
if *cmd == SlashCommand::EditPrompt {
|
||||
self.open_external_editor();
|
||||
self.command_popup = None;
|
||||
return (InputResult::None, true);
|
||||
}
|
||||
if !args.is_empty()
|
||||
&& (*cmd == SlashCommand::MountAdd || *cmd == SlashCommand::MountRemove)
|
||||
{
|
||||
let ev = if *cmd == SlashCommand::MountAdd {
|
||||
AppEvent::InlineMountAdd(args.to_string())
|
||||
} else {
|
||||
AppEvent::InlineMountRemove(args.to_string())
|
||||
};
|
||||
self.app_event_tx.send(ev);
|
||||
} else {
|
||||
self.app_event_tx.send(AppEvent::DispatchCommand(*cmd));
|
||||
}
|
||||
self.textarea.select_all();
|
||||
self.textarea.cut();
|
||||
|
||||
// Hide popup since the command has been dispatched.
|
||||
self.command_popup = None;
|
||||
return (InputResult::None, true);
|
||||
}
|
||||
@@ -211,6 +283,18 @@ impl ChatComposer<'_> {
|
||||
self.textarea.insert_newline();
|
||||
(InputResult::None, true)
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('m'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
shift: false,
|
||||
} => {
|
||||
// Toggle shell-command mode and prompt/exit accordingly
|
||||
self.shell_mode = !self.shell_mode;
|
||||
self.app_event_tx
|
||||
.send(AppEvent::DispatchCommand(SlashCommand::Shell));
|
||||
(InputResult::None, true)
|
||||
}
|
||||
input => self.handle_input_basic(input),
|
||||
}
|
||||
}
|
||||
@@ -221,6 +305,54 @@ impl ChatComposer<'_> {
|
||||
(InputResult::None, true)
|
||||
}
|
||||
|
||||
/// Launch an external editor on a temporary file pre-populated with the current draft,
|
||||
/// then reload the edited contents back into the textarea on exit.
|
||||
pub fn open_external_editor(&mut self) {
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
// Dump current draft to a temp file
|
||||
let content = self.textarea.lines().join("\n");
|
||||
let mut tmp = match tempfile::NamedTempFile::new() {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
tracing::error!("failed to create temp file for editor: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = write!(tmp, "{}", content) {
|
||||
tracing::error!("failed to write to temp file for editor: {e}");
|
||||
return;
|
||||
}
|
||||
let path = tmp.path();
|
||||
// Determine editor: VISUAL > EDITOR > nvim
|
||||
let editor = std::env::var("VISUAL")
|
||||
.or_else(|_| std::env::var("EDITOR"))
|
||||
.unwrap_or_else(|_| "nvim".into());
|
||||
// Launch editor and wait for exit
|
||||
if let Err(e) = Command::new(editor).arg(path).status() {
|
||||
tracing::error!("failed to launch editor: {e}");
|
||||
return;
|
||||
}
|
||||
// Read back edited contents (fall back to original on error)
|
||||
let new_text = std::fs::read_to_string(path).unwrap_or(content);
|
||||
// Replace textarea contents
|
||||
self.textarea.select_all();
|
||||
self.textarea.cut();
|
||||
let _ = self.textarea.insert_str(new_text);
|
||||
}
|
||||
|
||||
/// Return the current text in the composer input.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_input_text(&self) -> String {
|
||||
self.textarea.lines().join("\n")
|
||||
}
|
||||
|
||||
/// Returns true if the composer is in shell-command mode.
|
||||
#[allow(dead_code)]
|
||||
pub fn is_shell_mode(&self) -> bool {
|
||||
self.shell_mode
|
||||
}
|
||||
|
||||
/// Synchronize `self.command_popup` with the current text in the
|
||||
/// textarea. This must be called after every modification that can change
|
||||
/// the text so the popup is shown/updated/hidden as appropriate.
|
||||
@@ -249,14 +381,21 @@ impl ChatComposer<'_> {
|
||||
}
|
||||
|
||||
pub fn calculate_required_height(&self, area: &Rect) -> u16 {
|
||||
let rows = self.textarea.lines().len().max(MIN_TEXTAREA_ROWS);
|
||||
let rows = self
|
||||
.textarea
|
||||
.lines()
|
||||
.len()
|
||||
.max(MIN_TEXTAREA_ROWS)
|
||||
.min(self.max_rows);
|
||||
let num_popup_rows = if let Some(popup) = &self.command_popup {
|
||||
popup.calculate_required_height(area)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
rows as u16 + BORDER_LINES + num_popup_rows
|
||||
// Include an extra row for the context-left indicator when not in popup mode
|
||||
let context_row = if self.command_popup.is_none() { 1 } else { 0 };
|
||||
rows as u16 + BORDER_LINES + num_popup_rows + context_row
|
||||
}
|
||||
|
||||
fn update_border(&mut self, has_focus: bool) {
|
||||
@@ -265,7 +404,13 @@ impl ChatComposer<'_> {
|
||||
border_style: Style,
|
||||
}
|
||||
|
||||
let bs = if has_focus {
|
||||
let bs = if self.shell_mode {
|
||||
BlockState {
|
||||
right_title: Line::from("Shell mode – Enter to run | Ctrl+M to exit shell mode")
|
||||
.alignment(Alignment::Right),
|
||||
border_style: Style::default().fg(Color::Red),
|
||||
}
|
||||
} else if has_focus {
|
||||
BlockState {
|
||||
right_title: Line::from("Enter to send | Ctrl+D to quit | Ctrl+J for newline")
|
||||
.alignment(Alignment::Right),
|
||||
@@ -281,7 +426,7 @@ impl ChatComposer<'_> {
|
||||
self.textarea.set_block(
|
||||
ratatui::widgets::Block::default()
|
||||
.title_bottom(bs.right_title)
|
||||
.borders(Borders::ALL)
|
||||
.borders(Borders::BOTTOM)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(bs.border_style),
|
||||
);
|
||||
@@ -318,5 +463,23 @@ impl WidgetRef for &ChatComposer<'_> {
|
||||
} else {
|
||||
self.textarea.render(area, buf);
|
||||
}
|
||||
// Render context-left indicator when not displaying a popup
|
||||
if self.command_popup.is_none() {
|
||||
let pct = self.context_left_percent.round();
|
||||
let text = format!("{:.0}% context left", pct);
|
||||
let color = if pct > 40.0 {
|
||||
Color::Green
|
||||
} else if pct > 25.0 {
|
||||
Color::Yellow
|
||||
} else {
|
||||
Color::Red
|
||||
};
|
||||
buf.set_string(
|
||||
area.x + 1,
|
||||
area.y + area.height - 1,
|
||||
text,
|
||||
Style::default().fg(color),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,29 @@ pub(crate) struct CommandPopup {
|
||||
selected_idx: Option<usize>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::slash_command::SlashCommand;
|
||||
|
||||
#[test]
|
||||
fn filter_inspect_env_in_command_popup() {
|
||||
let mut popup = CommandPopup::new();
|
||||
popup.on_composer_text_change("/inspect-env".to_string());
|
||||
let filtered: Vec<&SlashCommand> = popup.filtered_commands();
|
||||
// Ensure InspectEnv command is among filtered results
|
||||
assert!(filtered.contains(&&SlashCommand::InspectEnv));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_inspect_env_as_selected_command() {
|
||||
let mut popup = CommandPopup::new();
|
||||
popup.on_composer_text_change("/inspect-env".to_string());
|
||||
popup.selected_idx = Some(0);
|
||||
assert_eq!(popup.selected_command(), Some(&SlashCommand::InspectEnv));
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandPopup {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
|
||||
66
codex-rs/tui/src/bottom_pane/config_reload_view.rs
Normal file
66
codex-rs/tui/src/bottom_pane/config_reload_view.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::prelude::Widget;
|
||||
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||
|
||||
use super::{BottomPane, BottomPaneView};
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
|
||||
/// BottomPane view displaying the diff and prompting to apply or ignore.
|
||||
pub(crate) struct ConfigReloadView {
|
||||
diff: String,
|
||||
app_event_tx: AppEventSender,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
impl ConfigReloadView {
|
||||
/// Create a new view with the unified diff of config changes.
|
||||
pub fn new(diff: String, app_event_tx: AppEventSender) -> Self {
|
||||
Self {
|
||||
diff,
|
||||
app_event_tx,
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BottomPaneView<'a> for ConfigReloadView {
|
||||
fn handle_key_event(&mut self, pane: &mut BottomPane<'a>, key_event: KeyEvent) {
|
||||
match key_event.code {
|
||||
KeyCode::Enter => {
|
||||
self.app_event_tx.send(AppEvent::ConfigReloadApply);
|
||||
self.done = true;
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
self.app_event_tx.send(AppEvent::ConfigReloadIgnore);
|
||||
self.done = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
pane.request_redraw();
|
||||
}
|
||||
|
||||
fn is_complete(&self) -> bool {
|
||||
self.done
|
||||
}
|
||||
|
||||
fn calculate_required_height(&self, area: &Rect) -> u16 {
|
||||
area.height
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Config changed (Enter=Apply Esc=Ignore)");
|
||||
Paragraph::new(self.diff.clone())
|
||||
.block(block)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn should_hide_when_task_is_done(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user