Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion nori-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion nori-rs/acp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ rmcp = { workspace = true, features = [
"transport-streamable-http-server",
] }
base64 = { workspace = true }
codex-core = { workspace = true }
codex-git = { workspace = true }
codex-rmcp-client = { workspace = true }
codex-protocol = { workspace = true }
Expand All @@ -42,6 +41,11 @@ toml = { workspace = true }
toml_edit = { workspace = true }
dirs = { workspace = true }
diffy = { workspace = true }
notify-rust = { workspace = true }
regex = { workspace = true }
shlex = { workspace = true }
tree-sitter = { workspace = true }
tree-sitter-bash = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
which = { workspace = true }
Expand Down
17 changes: 12 additions & 5 deletions nori-rs/acp/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Path: @/nori-rs/acp
- The ACP crate implements the Agent Client Protocol integration for Nori. It manages connecting to ACP-compliant agents by spawning local subprocesses (like Claude Code, Codex, or Gemini), communicating with them over JSON-RPC via stdin/stdout, and normalizing ACP session-domain data into `nori_protocol::ClientEvent` for the TUI and transcript layers.
- It owns ACP backend session state that is not provided by agents, including per-session thread goals used by the `/goal` TUI command and prompt-context injection.
- `codex_protocol::EventMsg` remains only for narrow control-plane concerns that are not ACP session semantics.
- Since the crate-layering cleanup (`@/docs/specs/crate-layering.md`), the crate has **no dependency on `codex-core`**. Its only inherited-Codex dependencies are the `codex-protocol` type vocabulary and `codex-rmcp-client`'s OAuth token store. Formerly-core leaf helpers now live here: user notifications (`user_notification.rs`), custom prompt discovery (`custom_prompts.rs`), shell/command parsing (`shell.rs`, `bash.rs`, `powershell.rs`, `parse_command/`), compact summarization constants and templates (`compact.rs`, `templates/compact/`), and patch construction (`patch.rs`, `create_patch_with_context`).

### How it fits into the larger codebase

Expand Down Expand Up @@ -35,7 +36,7 @@ Key files:
- `registry.rs` - Agent configuration and npm package detection
- `connection/` - ACP SDK (`agent-client-protocol`) based agent communication over the stdio of a spawned subprocess, including child lifecycle ownership (see `@/nori-rs/acp/src/connection/docs.md`)
- `translator.rs` - User input to ACP `ContentBlock` conversion and related parsing helpers
- `backend/mod.rs` - Implements `ConversationClient` trait from codex-core and emits normalized ACP session events
- `backend/mod.rs` - Owns `AcpBackend`, which serves the shared `Op`/`Event` contract from `@/nori-rs/protocol/` and emits normalized ACP session events
- `backend/thread_goal.rs` - Owns per-session `/goal` state, prompt goal-context formatting, transcript rehydration, and usage checkpoint updates
- `backend/nori_client_mcp.rs` - Hosts the `nori-client` MCP server: typed `#[tool]` goal handlers on `NoriClientService` (an rmcp `ServerHandler`), MCP resource/prompt handlers backed by `backend/nori_client_context.rs`, and rmcp's `StreamableHttpService` over a loopback `axum` listener (`NoriClientServer`)
- `transcript_discovery.rs` - Discovers transcript files for external agents
Expand Down Expand Up @@ -231,6 +232,10 @@ Three config enums control notification behavior, all stored in the `[tui]` sect

The `AcpBackendConfig` struct carries both `os_notifications` and `notify_after_idle` so the backend can configure the `UserNotifier` and the idle timer respectively. Terminal notifications flow separately through `codex-core`'s `Config::tui_notifications` bool to the TUI's `ChatWidget::notify()` method.

**User Notifications** (`user_notification.rs`):

`UserNotifier` delivers OS-level notifications for turn completion, awaiting approval, and session idle. It supports two modes: native desktop notifications via `notify-rust` (gated by `use_native`, driven by the `OsNotifications` config enum) and an external user-configured `notify_command` script that receives a JSON payload. Native sends are deliberately non-blocking -- `send_native()` spawns a background thread because `notif.show()` blocks synchronously on some platforms (notably macOS); on X11 Linux that thread also handles click-to-focus via `wmctrl`/`xdotool`. This module moved here from `codex-core` because the ACP backend is its only consumer.

**TUI Display Configuration** (`config/types/mod.rs`):

The `[tui]` section also owns display-only preferences consumed by `@/nori-rs/tui/`. `custom_working_messages` defaults to `true`; setting it to `false` disables the rotating whimsical status header list and lets the TUI use a plain "Working" label while a task starts. The companion `custom_working_message_list` accepts an array of strings; when non-empty and `custom_working_messages` is `true`, the TUI samples from the user's list instead of the builtin whimsical messages. Both values are resolved onto `NoriConfig` in `loader.rs` and mirrored through `codex-core`'s config. The `/config` menu only toggles the boolean; the user list is TOML-only and the menu's "Custom Working Messages" entry advertises when a custom list is active.
Expand Down Expand Up @@ -513,11 +518,11 @@ Async hooks fire at the same lifecycle points as their synchronous counterparts,

**Custom Prompts** (`backend/mod.rs`):

When the TUI sends `Op::ListCustomPrompts`, the ACP backend discovers prompt files (`.md`, `.sh`, `.py`, `.js`) from `{nori_home}/commands/` and returns them via `ListCustomPromptsResponse`. This reuses `codex_core::custom_prompts::discover_prompts_in()` from `@/nori-rs/core/src/custom_prompts.rs` for filesystem discovery. Markdown files have their frontmatter parsed for metadata; script files are returned with empty content and a `CustomPromptKind::Script` kind. The handler spawns an async task and sends results through the existing `event_tx` channel. The TUI receives these prompts in `ChatWidget::on_list_custom_prompts()` and populates the slash command popup.
When the TUI sends `Op::ListCustomPrompts`, the ACP backend discovers prompt files (`.md`, `.sh`, `.py`, `.js`) from `{nori_home}/commands/` and returns them via `ListCustomPromptsResponse`. Filesystem discovery lives in this crate: `discover_prompts_in()` in `@/nori-rs/acp/src/custom_prompts.rs` scans the directory, parses Markdown frontmatter for `description` and `argument_hint`, and assigns script interpreters by extension (`.sh` -> `bash`, `.py` -> `python3`, `.js` -> `node`) using the `CustomPromptKind` types from `@/nori-rs/protocol/src/custom_prompts.rs`. Script prompts are returned with empty content; `execute_script()` (called later by the TUI) runs the script via its interpreter with a configurable timeout and captures stdout. The handler spawns an async task and sends results through the existing `event_tx` channel. The TUI receives these prompts in `ChatWidget::on_list_custom_prompts()` and populates the slash command popup.

When the TUI sends `Op::RunUserShellCommand` (from prompt-initial `!cmd`), the ACP backend starts the command locally in the session working directory using the user's shell and detaches it from the `submit()` call so the TUI can keep accepting composer edits while the command runs. This is intentionally local Nori behavior rather than an ACP agent request: the background command task emits `TaskStarted`, `ExecCommandBegin`, any stdout/stderr `ExecCommandOutputDelta` events, `ExecCommandEnd`, and finally `TaskComplete` so the shared TUI exec cell path renders the result and returns the session to prompt-ready state.

Note: The ACP backend uses `{nori_home}/commands/` (e.g., `~/.nori/cli/commands/`) rather than `~/.codex/prompts/` which is used by the HTTP/codex-core backend.
Note: The ACP backend uses `{nori_home}/commands/` (e.g., `~/.nori/cli/commands/`) rather than upstream Codex's `~/.codex/prompts/` convention.

**Transcript Discovery** (`transcript_discovery.rs`):

Expand Down Expand Up @@ -697,7 +702,7 @@ The ACP connection layer uses the official `agent-client-protocol` SDK (0.15.1,

**MCP Server Forwarding and the Backend-Owned `nori-client` MCP Server** (`connection/mcp.rs`, `backend/nori_client_mcp.rs`):

CLI-configured MCP servers (from `config.toml`) are converted to ACP schema types and passed to the agent via `NewSessionRequest.mcp_servers` at session creation time. The `to_acp_mcp_servers()` function in `connection/mcp.rs` bridges `codex_core::config::types::McpServerConfig` to ACP `McpServer` values inside the transport adapter:
CLI-configured MCP servers (from `config.toml`) are converted to ACP schema types and passed to the agent via `NewSessionRequest.mcp_servers` at session creation time. The `to_acp_mcp_servers()` function in `connection/mcp.rs` bridges `codex_protocol::config_types::McpServerConfig` (`@/nori-rs/protocol/src/config_types.rs`) to ACP `McpServer` values inside the transport adapter:

| Transport | ACP Type | Key Fields |
| ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------- |
Expand Down Expand Up @@ -1026,6 +1031,8 @@ Unlike core's direct history manipulation, ACP uses a **prompt-based approach**:
4. The `ContextCompactedEvent` is emitted with the summary text cloned from `pending_compact_summary`, enabling the TUI to render a visual session boundary
5. Summary is prepended to the next user message (via `SUMMARY_PREFIX` framing)

The `SUMMARIZATION_PROMPT` and `SUMMARY_PREFIX` constants are crate-local, loaded from the prompt templates in `@/nori-rs/acp/templates/compact/` by `@/nori-rs/acp/src/compact.rs`.

The `ContextCompactedEvent.summary` field is the coupling point between the ACP backend and the TUI's session boundary rendering. The TUI uses it to flush the streamed summary, show a "Context compacted" info message, insert a new session header, and reprint the summary as the first assistant message of the new session (see `@/nori-rs/tui/docs.md`).

**Session Resume** (`backend/mod.rs`, `connection.rs`):
Expand Down Expand Up @@ -1165,7 +1172,7 @@ Large modules use a directory layout (`foo/mod.rs` + submodules) instead of a si
- The minimum supported ACP protocol version is V1 (`MINIMUM_SUPPORTED_VERSION`); initialize is sent with `ProtocolVersion::LATEST`
- `nori-acp` no longer has an `unstable` feature. Model selection rides the `SessionConfigOptionCategory::Model` variant, which the schema exposes only behind its own `unstable` feature; `nori-acp` turns that on unconditionally (`agent-client-protocol-schema = { features = ["unstable"] }` in `Cargo.toml`). The removed `unstable` feature previously gated only the deleted `session/set_model`/`SessionModelState` model-selection API
- Approval requests are translated to use appropriate UI (exec approval for shell commands, patch approval for file edits)
- Config loading uses Nori-specific paths (`~/.nori/cli/config.toml`) when the `nori-config` feature is enabled in the TUI
- Config loading uses Nori-specific paths (`~/.nori/cli/config.toml`) unconditionally; the TUI's old `nori-config` cargo feature and its legacy codex-config fallback branches were removed
- Transcript discovery is synchronous and intended for use in background threads (e.g., the TUI's `SystemInfo` collection thread)
- Transcript discovery for all agents requires the first user message to function correctly; without it, the discovery returns an error. This is enforced via shell-based search using `rg` or `grep`.

Expand Down
4 changes: 2 additions & 2 deletions nori-rs/acp/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use std::sync::atomic::Ordering;

use agent_client_protocol_schema::v1 as acp;
use anyhow::Result;
use codex_core::config::types::McpServerConfig;
use codex_protocol::ConversationId;
use codex_protocol::config_types::McpServerConfig;
#[cfg(test)]
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::AskForApproval;
Expand Down Expand Up @@ -310,7 +310,7 @@ pub struct AcpBackend {
/// Pending approval requests waiting for user decision
pending_approvals: Arc<Mutex<Vec<PendingApprovalRequest>>>,
/// Notifier for OS-level notifications (approval waiting, idle)
user_notifier: Arc<codex_core::UserNotifier>,
user_notifier: Arc<crate::UserNotifier>,
/// Abort handle for the idle detection timer (if running)
idle_timer_abort: Arc<Mutex<Option<tokio::task::AbortHandle>>>,
/// Nori home directory for history storage
Expand Down
2 changes: 1 addition & 1 deletion nori-rs/acp/src/backend/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ impl AcpBackend {
let (prompt_result_tx, prompt_result_rx) = mpsc::channel(128);
let use_native_notifications =
config.os_notifications == crate::config::OsNotifications::Enabled;
let user_notifier = Arc::new(codex_core::UserNotifier::new(
let user_notifier = Arc::new(crate::UserNotifier::new(
config.notify.clone(),
use_native_notifications,
));
Expand Down
4 changes: 2 additions & 2 deletions nori-rs/acp/src/backend/session_runtime_driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ impl AcpBackend {

self.pending_approvals.lock().await.push(*pending_request);
self.user_notifier
.notify(&codex_core::UserNotification::AwaitingApproval {
.notify(&crate::UserNotification::AwaitingApproval {
call_id: notification_call_id,
command: command_for_notification,
cwd: self.cwd.display().to_string(),
Expand Down Expand Up @@ -848,7 +848,7 @@ impl AcpBackend {
let session_id = self.session_id.read().await.to_string();
let idle_task = tokio::spawn(async move {
tokio::time::sleep(duration).await;
user_notifier.notify(&codex_core::UserNotification::Idle {
user_notifier.notify(&crate::UserNotification::Idle {
session_id,
idle_duration_secs: idle_secs,
});
Expand Down
4 changes: 2 additions & 2 deletions nori-rs/acp/src/backend/spawn_and_relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ impl AcpBackend {
let (prompt_result_tx, prompt_result_rx) = mpsc::channel(128);
let use_native_notifications =
config.os_notifications == crate::config::OsNotifications::Enabled;
let user_notifier = Arc::new(codex_core::UserNotifier::new(
let user_notifier = Arc::new(crate::UserNotifier::new(
config.notify.clone(),
use_native_notifications,
));
Expand Down Expand Up @@ -412,7 +412,7 @@ impl AcpBackend {
backend: AcpBackend,
mut approval_rx: mpsc::Receiver<ApprovalRequest>,
_pending_approvals: Arc<Mutex<Vec<PendingApprovalRequest>>>,
_user_notifier: Arc<codex_core::UserNotifier>,
_user_notifier: Arc<crate::UserNotifier>,
approval_policy_rx: watch::Receiver<AskForApproval>,
) {
let approval_policy_rx = approval_policy_rx;
Expand Down
5 changes: 2 additions & 3 deletions nori-rs/acp/src/backend/submit_and_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,7 @@ impl AcpBackend {
let event_tx = self.event_tx.clone();
let id_clone = id.clone();
tokio::spawn(async move {
let custom_prompts =
codex_core::custom_prompts::discover_prompts_in(&dir).await;
let custom_prompts = crate::custom_prompts::discover_prompts_in(&dir).await;
let _ = event_tx
.send(Event {
id: id_clone,
Expand Down Expand Up @@ -275,7 +274,7 @@ impl AcpBackend {
/// 3. Store it in pending_compact_summary
/// 4. Emit ContextCompacted and Warning events
pub(super) async fn handle_compact(&self, id: &str) -> Result<()> {
use codex_core::compact::SUMMARIZATION_PROMPT;
use crate::compact::SUMMARIZATION_PROMPT;

let _ = self
.session_event_tx
Expand Down
2 changes: 1 addition & 1 deletion nori-rs/acp/src/backend/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ fn spawn_test_approval_handler(
event_tx: mpsc::Sender<Event>,
client_event_tx: Option<mpsc::Sender<nori_protocol::ClientEvent>>,
pending_approvals: Arc<Mutex<Vec<PendingApprovalRequest>>>,
user_notifier: Arc<codex_core::UserNotifier>,
user_notifier: Arc<crate::UserNotifier>,
approval_policy_rx: watch::Receiver<AskForApproval>,
) {
let (backend_event_tx, backend_event_rx) = mpsc::channel(64);
Expand Down
8 changes: 4 additions & 4 deletions nori-rs/acp/src/backend/tests/part3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ async fn test_approval_policy_dynamic_update() {
let (event_tx, mut event_rx) = mpsc::channel::<Event>(16);
let (client_event_tx, mut client_event_rx) = mpsc::channel::<nori_protocol::ClientEvent>(16);
let pending_approvals = Arc::new(Mutex::new(Vec::<PendingApprovalRequest>::new()));
let user_notifier = Arc::new(codex_core::UserNotifier::new(None, false));
let user_notifier = Arc::new(crate::UserNotifier::new(None, false));
let cwd = PathBuf::from("/tmp/test");

// Create watch channel starting with OnRequest policy (requires approval)
Expand Down Expand Up @@ -604,7 +604,7 @@ async fn test_patch_approval_emits_normalized_client_event() {
let (event_tx, mut event_rx) = mpsc::channel::<Event>(16);
let (client_event_tx, mut client_event_rx) = mpsc::channel::<nori_protocol::ClientEvent>(16);
let pending_approvals = Arc::new(Mutex::new(Vec::<PendingApprovalRequest>::new()));
let user_notifier = Arc::new(codex_core::UserNotifier::new(None, false));
let user_notifier = Arc::new(crate::UserNotifier::new(None, false));
let (_policy_tx, policy_rx) = watch::channel(AskForApproval::OnRequest);

spawn_test_approval_handler(
Expand Down Expand Up @@ -712,7 +712,7 @@ async fn test_exec_approval_emits_normalized_client_event() {
let (event_tx, mut event_rx) = mpsc::channel::<Event>(16);
let (client_event_tx, mut client_event_rx) = mpsc::channel::<nori_protocol::ClientEvent>(16);
let pending_approvals = Arc::new(Mutex::new(Vec::<PendingApprovalRequest>::new()));
let user_notifier = Arc::new(codex_core::UserNotifier::new(None, false));
let user_notifier = Arc::new(crate::UserNotifier::new(None, false));
let (_policy_tx, policy_rx) = watch::channel(AskForApproval::OnRequest);

spawn_test_approval_handler(
Expand Down Expand Up @@ -803,7 +803,7 @@ async fn test_exec_approval_with_never_policy_does_not_emit_normalized_client_ev
let (event_tx, mut event_rx) = mpsc::channel::<Event>(16);
let (client_event_tx, mut client_event_rx) = mpsc::channel::<nori_protocol::ClientEvent>(16);
let pending_approvals = Arc::new(Mutex::new(Vec::<PendingApprovalRequest>::new()));
let user_notifier = Arc::new(codex_core::UserNotifier::new(None, false));
let user_notifier = Arc::new(crate::UserNotifier::new(None, false));
let (_policy_tx, policy_rx) = watch::channel(AskForApproval::Never);

spawn_test_approval_handler(
Expand Down
2 changes: 1 addition & 1 deletion nori-rs/acp/src/backend/tests/part4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1056,7 +1056,7 @@ async fn test_list_custom_prompts_sends_response_event() {
let id = "test-id".to_string();

tokio::spawn(async move {
let custom_prompts = codex_core::custom_prompts::discover_prompts_in(&dir).await;
let custom_prompts = crate::custom_prompts::discover_prompts_in(&dir).await;
let _ = event_tx
.send(Event {
id,
Expand Down
2 changes: 1 addition & 1 deletion nori-rs/acp/src/backend/user_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ impl AcpBackend {
// Check if we have a pending compact summary to prepend
let pending_summary = self.pending_compact_summary.lock().await.take();
let final_prompt_text = if let Some(summary) = pending_summary {
use codex_core::compact::SUMMARY_PREFIX;
use crate::compact::SUMMARY_PREFIX;
format!("{SUMMARY_PREFIX}\n{summary}\n\n{prompt_with_goal_context}")
} else {
prompt_with_goal_context
Expand Down
2 changes: 1 addition & 1 deletion nori-rs/acp/src/backend/user_shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub(crate) async fn run_user_shell_command(
command: String,
) {
let argv = shell_command_argv(&command);
let parsed_cmd = codex_core::parse_command::parse_command(&argv);
let parsed_cmd = crate::parse_command::parse_command(&argv);
let call_id = format!("user-shell-{id}");
let started = Instant::now();

Expand Down
File renamed without changes.
File renamed without changes.
Loading
Loading