2025-08-11 12:31:34 -07:00
|
|
|
|
use crate::diff_render::create_diff_summary;
|
2025-08-06 14:36:48 -07:00
|
|
|
|
use crate::exec_command::relativize_to_home;
|
2025-07-31 00:43:21 -07:00
|
|
|
|
use crate::exec_command::strip_bash_lc_and_escape;
|
2025-08-20 17:09:46 -07:00
|
|
|
|
use crate::markdown::append_markdown;
|
2025-09-05 07:10:32 -07:00
|
|
|
|
use crate::render::line_utils::line_to_static;
|
2025-09-04 16:54:53 -07:00
|
|
|
|
use crate::render::line_utils::prefix_lines;
|
2025-09-05 07:10:32 -07:00
|
|
|
|
use crate::render::line_utils::push_owned_lines;
|
2025-08-06 14:36:48 -07:00
|
|
|
|
use crate::slash_command::SlashCommand;
|
2025-06-03 14:29:26 -07:00
|
|
|
|
use crate::text_formatting::format_and_truncate_tool_result;
|
2025-09-05 07:10:32 -07:00
|
|
|
|
use crate::wrapping::RtOptions;
|
|
|
|
|
|
use crate::wrapping::word_wrap_line;
|
|
|
|
|
|
use crate::wrapping::word_wrap_lines;
|
fix: introduce ResponseInputItem::McpToolCallOutput variant (#1151)
The output of an MCP server tool call can be one of several types, but
to date, we treated all outputs as text by showing the serialized JSON
as the "tool output" in Codex:
https://github.com/openai/codex/blob/25a9949c49194d5a64de54a11bcc5b4724ac9bd5/codex-rs/mcp-types/src/lib.rs#L96-L101
This PR adds support for the `ImageContent` variant so we can now
display an image output from an MCP tool call.
In making this change, we introduce a new
`ResponseInputItem::McpToolCallOutput` variant so that we can work with
the `mcp_types::CallToolResult` directly when the function call is made
to an MCP server.
Though arguably the more significant change is the introduction of
`HistoryCell::CompletedMcpToolCallWithImageOutput`, which is a cell that
uses `ratatui_image` to render an image into the terminal. To support
this, we introduce `ImageRenderCache`, cache a
`ratatui_image::picker::Picker`, and `ensure_image_cache()` to cache the
appropriate scaled image data and dimensions based on the current
terminal size.
To test, I created a minimal `package.json`:
```json
{
"name": "kitty-mcp",
"version": "1.0.0",
"type": "module",
"description": "MCP that returns image of kitty",
"main": "index.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0"
}
}
```
with the following `index.js` to define the MCP server:
```js
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
const IMAGE_URI = "image://Ada.png";
const server = new McpServer({
name: "Demo",
version: "1.0.0",
});
server.tool(
"get-cat-image",
"If you need a cat image, this tool will provide one.",
async () => ({
content: [
{ type: "image", data: await getAdaPngBase64(), mimeType: "image/png" },
],
})
);
server.resource("Ada the Cat", IMAGE_URI, async (uri) => {
const base64Image = await getAdaPngBase64();
return {
contents: [
{
uri: uri.href,
mimeType: "image/png",
blob: base64Image,
},
],
};
});
async function getAdaPngBase64() {
const __dirname = new URL(".", import.meta.url).pathname;
// From https://github.com/benjajaja/ratatui-image/blob/9705ce2c59ec669abbce2924cbfd1f5ae22c9860/assets/Ada.png
const filePath = join(__dirname, "Ada.png");
const imageData = await readFile(filePath);
const base64Image = imageData.toString("base64");
return base64Image;
}
const transport = new StdioServerTransport();
await server.connect(transport);
```
With the local changes from this PR, I added the following to my
`config.toml`:
```toml
[mcp_servers.kitty]
command = "node"
args = ["/Users/mbolin/code/kitty-mcp/index.js"]
```
Running the TUI from source:
```
cargo run --bin codex -- --model o3 'I need a picture of a cat'
```
I get:
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/bf80b721-9ca0-4d81-aec7-77d6899e2869"
/>
Now, that said, I have only tested in iTerm and there is definitely some
funny business with getting an accurate character-to-pixel ratio
(sometimes the `CompletedMcpToolCallWithImageOutput` thinks it needs 10
rows to render instead of 4), so there is still work to be done here.
2025-05-28 19:03:17 -07:00
|
|
|
|
use base64::Engine;
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
use codex_ansi_escape::ansi_escape_line;
|
2025-08-05 23:57:52 -07:00
|
|
|
|
use codex_common::create_config_summary_entries;
|
2025-05-06 17:38:56 -07:00
|
|
|
|
use codex_common::elapsed::format_duration;
|
2025-09-02 18:36:19 -07:00
|
|
|
|
use codex_core::auth::get_auth_file;
|
|
|
|
|
|
use codex_core::auth::try_read_auth_json;
|
2025-04-27 21:47:50 -07:00
|
|
|
|
use codex_core::config::Config;
|
2025-09-04 11:00:01 -07:00
|
|
|
|
use codex_core::config_types::ReasoningSummaryFormat;
|
2025-07-31 13:45:52 -07:00
|
|
|
|
use codex_core::plan_tool::PlanItemArg;
|
|
|
|
|
|
use codex_core::plan_tool::StepStatus;
|
|
|
|
|
|
use codex_core::plan_tool::UpdatePlanArgs;
|
2025-08-21 08:52:17 -07:00
|
|
|
|
use codex_core::project_doc::discover_project_doc_paths;
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
use codex_core::protocol::FileChange;
|
2025-07-30 10:05:40 -07:00
|
|
|
|
use codex_core::protocol::McpInvocation;
|
2025-08-07 04:02:58 -07:00
|
|
|
|
use codex_core::protocol::SandboxPolicy;
|
2025-05-13 19:22:16 -07:00
|
|
|
|
use codex_core::protocol::SessionConfiguredEvent;
|
2025-08-05 23:57:52 -07:00
|
|
|
|
use codex_core::protocol::TokenUsage;
|
2025-09-07 20:22:25 -07:00
|
|
|
|
use codex_protocol::mcp_protocol::ConversationId;
|
2025-09-08 14:48:48 -07:00
|
|
|
|
use codex_protocol::num_format::format_with_separators;
|
2025-08-15 12:44:40 -07:00
|
|
|
|
use codex_protocol::parse_command::ParsedCommand;
|
fix: introduce ResponseInputItem::McpToolCallOutput variant (#1151)
The output of an MCP server tool call can be one of several types, but
to date, we treated all outputs as text by showing the serialized JSON
as the "tool output" in Codex:
https://github.com/openai/codex/blob/25a9949c49194d5a64de54a11bcc5b4724ac9bd5/codex-rs/mcp-types/src/lib.rs#L96-L101
This PR adds support for the `ImageContent` variant so we can now
display an image output from an MCP tool call.
In making this change, we introduce a new
`ResponseInputItem::McpToolCallOutput` variant so that we can work with
the `mcp_types::CallToolResult` directly when the function call is made
to an MCP server.
Though arguably the more significant change is the introduction of
`HistoryCell::CompletedMcpToolCallWithImageOutput`, which is a cell that
uses `ratatui_image` to render an image into the terminal. To support
this, we introduce `ImageRenderCache`, cache a
`ratatui_image::picker::Picker`, and `ensure_image_cache()` to cache the
appropriate scaled image data and dimensions based on the current
terminal size.
To test, I created a minimal `package.json`:
```json
{
"name": "kitty-mcp",
"version": "1.0.0",
"type": "module",
"description": "MCP that returns image of kitty",
"main": "index.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0"
}
}
```
with the following `index.js` to define the MCP server:
```js
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
const IMAGE_URI = "image://Ada.png";
const server = new McpServer({
name: "Demo",
version: "1.0.0",
});
server.tool(
"get-cat-image",
"If you need a cat image, this tool will provide one.",
async () => ({
content: [
{ type: "image", data: await getAdaPngBase64(), mimeType: "image/png" },
],
})
);
server.resource("Ada the Cat", IMAGE_URI, async (uri) => {
const base64Image = await getAdaPngBase64();
return {
contents: [
{
uri: uri.href,
mimeType: "image/png",
blob: base64Image,
},
],
};
});
async function getAdaPngBase64() {
const __dirname = new URL(".", import.meta.url).pathname;
// From https://github.com/benjajaja/ratatui-image/blob/9705ce2c59ec669abbce2924cbfd1f5ae22c9860/assets/Ada.png
const filePath = join(__dirname, "Ada.png");
const imageData = await readFile(filePath);
const base64Image = imageData.toString("base64");
return base64Image;
}
const transport = new StdioServerTransport();
await server.connect(transport);
```
With the local changes from this PR, I added the following to my
`config.toml`:
```toml
[mcp_servers.kitty]
command = "node"
args = ["/Users/mbolin/code/kitty-mcp/index.js"]
```
Running the TUI from source:
```
cargo run --bin codex -- --model o3 'I need a picture of a cat'
```
I get:
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/bf80b721-9ca0-4d81-aec7-77d6899e2869"
/>
Now, that said, I have only tested in iTerm and there is definitely some
funny business with getting an accurate character-to-pixel ratio
(sometimes the `CompletedMcpToolCallWithImageOutput` thinks it needs 10
rows to render instead of 4), so there is still work to be done here.
2025-05-28 19:03:17 -07:00
|
|
|
|
use image::DynamicImage;
|
|
|
|
|
|
use image::ImageReader;
|
2025-09-02 10:29:58 -07:00
|
|
|
|
use itertools::Itertools;
|
2025-06-03 14:29:26 -07:00
|
|
|
|
use mcp_types::EmbeddedResourceResource;
|
2025-07-19 00:09:34 -04:00
|
|
|
|
use mcp_types::ResourceLink;
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
use ratatui::prelude::*;
|
|
|
|
|
|
use ratatui::style::Modifier;
|
|
|
|
|
|
use ratatui::style::Style;
|
2025-09-02 10:29:58 -07:00
|
|
|
|
use ratatui::style::Styled;
|
2025-08-22 16:32:31 -07:00
|
|
|
|
use ratatui::style::Stylize;
|
2025-08-06 12:03:45 -07:00
|
|
|
|
use ratatui::widgets::Paragraph;
|
|
|
|
|
|
use ratatui::widgets::WidgetRef;
|
2025-08-07 18:38:39 -07:00
|
|
|
|
use ratatui::widgets::Wrap;
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
use std::collections::HashMap;
|
fix: introduce ResponseInputItem::McpToolCallOutput variant (#1151)
The output of an MCP server tool call can be one of several types, but
to date, we treated all outputs as text by showing the serialized JSON
as the "tool output" in Codex:
https://github.com/openai/codex/blob/25a9949c49194d5a64de54a11bcc5b4724ac9bd5/codex-rs/mcp-types/src/lib.rs#L96-L101
This PR adds support for the `ImageContent` variant so we can now
display an image output from an MCP tool call.
In making this change, we introduce a new
`ResponseInputItem::McpToolCallOutput` variant so that we can work with
the `mcp_types::CallToolResult` directly when the function call is made
to an MCP server.
Though arguably the more significant change is the introduction of
`HistoryCell::CompletedMcpToolCallWithImageOutput`, which is a cell that
uses `ratatui_image` to render an image into the terminal. To support
this, we introduce `ImageRenderCache`, cache a
`ratatui_image::picker::Picker`, and `ensure_image_cache()` to cache the
appropriate scaled image data and dimensions based on the current
terminal size.
To test, I created a minimal `package.json`:
```json
{
"name": "kitty-mcp",
"version": "1.0.0",
"type": "module",
"description": "MCP that returns image of kitty",
"main": "index.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0"
}
}
```
with the following `index.js` to define the MCP server:
```js
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
const IMAGE_URI = "image://Ada.png";
const server = new McpServer({
name: "Demo",
version: "1.0.0",
});
server.tool(
"get-cat-image",
"If you need a cat image, this tool will provide one.",
async () => ({
content: [
{ type: "image", data: await getAdaPngBase64(), mimeType: "image/png" },
],
})
);
server.resource("Ada the Cat", IMAGE_URI, async (uri) => {
const base64Image = await getAdaPngBase64();
return {
contents: [
{
uri: uri.href,
mimeType: "image/png",
blob: base64Image,
},
],
};
});
async function getAdaPngBase64() {
const __dirname = new URL(".", import.meta.url).pathname;
// From https://github.com/benjajaja/ratatui-image/blob/9705ce2c59ec669abbce2924cbfd1f5ae22c9860/assets/Ada.png
const filePath = join(__dirname, "Ada.png");
const imageData = await readFile(filePath);
const base64Image = imageData.toString("base64");
return base64Image;
}
const transport = new StdioServerTransport();
await server.connect(transport);
```
With the local changes from this PR, I added the following to my
`config.toml`:
```toml
[mcp_servers.kitty]
command = "node"
args = ["/Users/mbolin/code/kitty-mcp/index.js"]
```
Running the TUI from source:
```
cargo run --bin codex -- --model o3 'I need a picture of a cat'
```
I get:
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/bf80b721-9ca0-4d81-aec7-77d6899e2869"
/>
Now, that said, I have only tested in iTerm and there is definitely some
funny business with getting an accurate character-to-pixel ratio
(sometimes the `CompletedMcpToolCallWithImageOutput` thinks it needs 10
rows to render instead of 4), so there is still work to be done here.
2025-05-28 19:03:17 -07:00
|
|
|
|
use std::io::Cursor;
|
2025-09-02 10:29:58 -07:00
|
|
|
|
use std::path::Path;
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
use std::time::Duration;
|
2025-08-14 19:32:45 -04:00
|
|
|
|
use std::time::Instant;
|
fix: introduce ResponseInputItem::McpToolCallOutput variant (#1151)
The output of an MCP server tool call can be one of several types, but
to date, we treated all outputs as text by showing the serialized JSON
as the "tool output" in Codex:
https://github.com/openai/codex/blob/25a9949c49194d5a64de54a11bcc5b4724ac9bd5/codex-rs/mcp-types/src/lib.rs#L96-L101
This PR adds support for the `ImageContent` variant so we can now
display an image output from an MCP tool call.
In making this change, we introduce a new
`ResponseInputItem::McpToolCallOutput` variant so that we can work with
the `mcp_types::CallToolResult` directly when the function call is made
to an MCP server.
Though arguably the more significant change is the introduction of
`HistoryCell::CompletedMcpToolCallWithImageOutput`, which is a cell that
uses `ratatui_image` to render an image into the terminal. To support
this, we introduce `ImageRenderCache`, cache a
`ratatui_image::picker::Picker`, and `ensure_image_cache()` to cache the
appropriate scaled image data and dimensions based on the current
terminal size.
To test, I created a minimal `package.json`:
```json
{
"name": "kitty-mcp",
"version": "1.0.0",
"type": "module",
"description": "MCP that returns image of kitty",
"main": "index.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0"
}
}
```
with the following `index.js` to define the MCP server:
```js
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
const IMAGE_URI = "image://Ada.png";
const server = new McpServer({
name: "Demo",
version: "1.0.0",
});
server.tool(
"get-cat-image",
"If you need a cat image, this tool will provide one.",
async () => ({
content: [
{ type: "image", data: await getAdaPngBase64(), mimeType: "image/png" },
],
})
);
server.resource("Ada the Cat", IMAGE_URI, async (uri) => {
const base64Image = await getAdaPngBase64();
return {
contents: [
{
uri: uri.href,
mimeType: "image/png",
blob: base64Image,
},
],
};
});
async function getAdaPngBase64() {
const __dirname = new URL(".", import.meta.url).pathname;
// From https://github.com/benjajaja/ratatui-image/blob/9705ce2c59ec669abbce2924cbfd1f5ae22c9860/assets/Ada.png
const filePath = join(__dirname, "Ada.png");
const imageData = await readFile(filePath);
const base64Image = imageData.toString("base64");
return base64Image;
}
const transport = new StdioServerTransport();
await server.connect(transport);
```
With the local changes from this PR, I added the following to my
`config.toml`:
```toml
[mcp_servers.kitty]
command = "node"
args = ["/Users/mbolin/code/kitty-mcp/index.js"]
```
Running the TUI from source:
```
cargo run --bin codex -- --model o3 'I need a picture of a cat'
```
I get:
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/bf80b721-9ca0-4d81-aec7-77d6899e2869"
/>
Now, that said, I have only tested in iTerm and there is definitely some
funny business with getting an accurate character-to-pixel ratio
(sometimes the `CompletedMcpToolCallWithImageOutput` thinks it needs 10
rows to render instead of 4), so there is still work to be done here.
2025-05-28 19:03:17 -07:00
|
|
|
|
use tracing::error;
|
2025-09-02 10:29:58 -07:00
|
|
|
|
use unicode_width::UnicodeWidthStr;
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
|
2025-08-20 17:09:46 -07:00
|
|
|
|
#[derive(Clone, Debug)]
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
pub(crate) struct CommandOutput {
|
|
|
|
|
|
pub(crate) exit_code: i32,
|
|
|
|
|
|
pub(crate) stdout: String,
|
|
|
|
|
|
pub(crate) stderr: String,
|
2025-08-22 16:32:31 -07:00
|
|
|
|
pub(crate) formatted_output: String,
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-02 10:29:58 -07:00
|
|
|
|
#[derive(Clone, Debug)]
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
pub(crate) enum PatchEventType {
|
|
|
|
|
|
ApprovalRequest,
|
|
|
|
|
|
ApplyBegin { auto_approved: bool },
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
/// Represents an event to display in the conversation history. Returns its
|
|
|
|
|
|
/// `Vec<Line<'static>>` representation to make it easier to display in a
|
|
|
|
|
|
/// scrollable list.
|
2025-08-20 17:09:46 -07:00
|
|
|
|
pub(crate) trait HistoryCell: std::fmt::Debug + Send + Sync {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
fn display_lines(&self, width: u16) -> Vec<Line<'static>>;
|
2025-08-14 14:10:05 -04:00
|
|
|
|
|
2025-08-20 17:09:46 -07:00
|
|
|
|
fn transcript_lines(&self) -> Vec<Line<'static>> {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
self.display_lines(u16::MAX)
|
2025-08-20 17:09:46 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
fn desired_height(&self, width: u16) -> u16 {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
Paragraph::new(Text::from(self.display_lines(width)))
|
2025-08-14 14:10:05 -04:00
|
|
|
|
.wrap(Wrap { trim: false })
|
|
|
|
|
|
.line_count(width)
|
|
|
|
|
|
.try_into()
|
|
|
|
|
|
.unwrap_or(0)
|
2025-07-30 10:05:40 -07:00
|
|
|
|
}
|
2025-09-02 10:29:58 -07:00
|
|
|
|
|
|
|
|
|
|
fn is_stream_continuation(&self) -> bool {
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
|
pub(crate) struct UserHistoryCell {
|
|
|
|
|
|
message: String,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl HistoryCell for UserHistoryCell {
|
|
|
|
|
|
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
|
|
|
|
|
let mut lines: Vec<Line<'static>> = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
// Wrap the content first, then prefix each wrapped line with the marker.
|
|
|
|
|
|
let wrap_width = width.saturating_sub(1); // account for the ▌ prefix
|
|
|
|
|
|
let wrapped = textwrap::wrap(
|
|
|
|
|
|
&self.message,
|
|
|
|
|
|
textwrap::Options::new(wrap_width as usize)
|
2025-09-05 07:10:32 -07:00
|
|
|
|
.wrap_algorithm(textwrap::WrapAlgorithm::FirstFit), // Match textarea wrap
|
2025-09-02 10:29:58 -07:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
for line in wrapped {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec!["▌".cyan().dim(), line.to_string().dim()].into());
|
2025-09-02 10:29:58 -07:00
|
|
|
|
}
|
|
|
|
|
|
lines
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn transcript_lines(&self) -> Vec<Line<'static>> {
|
|
|
|
|
|
let mut lines: Vec<Line<'static>> = Vec::new();
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push("user".cyan().bold().into());
|
|
|
|
|
|
lines.extend(self.message.lines().map(|l| l.to_string().into()));
|
2025-09-02 10:29:58 -07:00
|
|
|
|
lines
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
|
pub(crate) struct AgentMessageCell {
|
|
|
|
|
|
lines: Vec<Line<'static>>,
|
|
|
|
|
|
is_first_line: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl AgentMessageCell {
|
|
|
|
|
|
pub(crate) fn new(lines: Vec<Line<'static>>, is_first_line: bool) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
lines,
|
|
|
|
|
|
is_first_line,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl HistoryCell for AgentMessageCell {
|
|
|
|
|
|
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
2025-09-05 07:10:32 -07:00
|
|
|
|
word_wrap_lines(
|
|
|
|
|
|
&self.lines,
|
|
|
|
|
|
RtOptions::new(width as usize)
|
|
|
|
|
|
.initial_indent(if self.is_first_line {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
"> ".into()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
" ".into()
|
2025-09-05 07:10:32 -07:00
|
|
|
|
})
|
|
|
|
|
|
.subsequent_indent(" ".into()),
|
|
|
|
|
|
)
|
2025-09-02 10:29:58 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn transcript_lines(&self) -> Vec<Line<'static>> {
|
|
|
|
|
|
let mut out: Vec<Line<'static>> = Vec::new();
|
|
|
|
|
|
if self.is_first_line {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
out.push("codex".magenta().bold().into());
|
2025-09-02 10:29:58 -07:00
|
|
|
|
}
|
|
|
|
|
|
out.extend(self.lines.clone());
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn is_stream_continuation(&self) -> bool {
|
|
|
|
|
|
!self.is_first_line
|
|
|
|
|
|
}
|
2025-07-30 10:05:40 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-20 17:09:46 -07:00
|
|
|
|
#[derive(Debug)]
|
2025-08-14 14:10:05 -04:00
|
|
|
|
pub(crate) struct PlainHistoryCell {
|
|
|
|
|
|
lines: Vec<Line<'static>>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl HistoryCell for PlainHistoryCell {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
fn display_lines(&self, _width: u16) -> Vec<Line<'static>> {
|
2025-08-14 14:10:05 -04:00
|
|
|
|
self.lines.clone()
|
2025-07-30 10:05:40 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-20 17:09:46 -07:00
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
|
pub(crate) struct TranscriptOnlyHistoryCell {
|
|
|
|
|
|
lines: Vec<Line<'static>>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl HistoryCell for TranscriptOnlyHistoryCell {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
fn display_lines(&self, _width: u16) -> Vec<Line<'static>> {
|
2025-08-20 17:09:46 -07:00
|
|
|
|
Vec::new()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn transcript_lines(&self) -> Vec<Line<'static>> {
|
|
|
|
|
|
self.lines.clone()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2025-09-02 10:29:58 -07:00
|
|
|
|
pub(crate) struct PatchHistoryCell {
|
|
|
|
|
|
event_type: PatchEventType,
|
|
|
|
|
|
changes: HashMap<PathBuf, FileChange>,
|
|
|
|
|
|
cwd: PathBuf,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl HistoryCell for PatchHistoryCell {
|
|
|
|
|
|
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
|
|
|
|
|
create_diff_summary(
|
|
|
|
|
|
&self.changes,
|
|
|
|
|
|
self.event_type.clone(),
|
|
|
|
|
|
&self.cwd,
|
|
|
|
|
|
width as usize,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub(crate) struct ExecCall {
|
|
|
|
|
|
pub(crate) call_id: String,
|
2025-08-11 12:40:12 -07:00
|
|
|
|
pub(crate) command: Vec<String>,
|
|
|
|
|
|
pub(crate) parsed: Vec<ParsedCommand>,
|
|
|
|
|
|
pub(crate) output: Option<CommandOutput>,
|
2025-08-14 19:32:45 -04:00
|
|
|
|
start_time: Option<Instant>,
|
2025-08-22 16:32:31 -07:00
|
|
|
|
duration: Option<Duration>,
|
2025-09-02 10:29:58 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
|
pub(crate) struct ExecCell {
|
|
|
|
|
|
calls: Vec<ExecCall>,
|
2025-08-11 12:40:12 -07:00
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
impl HistoryCell for ExecCell {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
|
|
|
|
|
if self.is_exploring_cell() {
|
|
|
|
|
|
self.exploring_display_lines(width)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_display_lines(width)
|
|
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}
|
2025-08-22 16:32:31 -07:00
|
|
|
|
|
|
|
|
|
|
fn transcript_lines(&self) -> Vec<Line<'static>> {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
let mut lines: Vec<Line<'static>> = vec![];
|
|
|
|
|
|
for call in &self.calls {
|
|
|
|
|
|
let cmd_display = strip_bash_lc_and_escape(&call.command);
|
|
|
|
|
|
for (i, part) in cmd_display.lines().enumerate() {
|
|
|
|
|
|
if i == 0 {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec!["$ ".magenta(), part.to_string().into()].into());
|
2025-09-02 10:29:58 -07:00
|
|
|
|
} else {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" ".into(), part.to_string().into()].into());
|
2025-09-02 10:29:58 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-22 16:32:31 -07:00
|
|
|
|
|
2025-09-02 10:29:58 -07:00
|
|
|
|
if let Some(output) = call.output.as_ref() {
|
|
|
|
|
|
lines.extend(output.formatted_output.lines().map(ansi_escape_line));
|
|
|
|
|
|
let duration = call
|
|
|
|
|
|
.duration
|
|
|
|
|
|
.map(format_duration)
|
|
|
|
|
|
.unwrap_or_else(|| "unknown".to_string());
|
2025-09-02 16:19:54 -07:00
|
|
|
|
let mut result: Line = if output.exit_code == 0 {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
Line::from("✓".green().bold())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Line::from(vec![
|
|
|
|
|
|
"✗".red().bold(),
|
|
|
|
|
|
format!(" ({})", output.exit_code).into(),
|
|
|
|
|
|
])
|
|
|
|
|
|
};
|
|
|
|
|
|
result.push_span(format!(" • {duration}").dim());
|
|
|
|
|
|
lines.push(result);
|
2025-08-22 16:32:31 -07:00
|
|
|
|
}
|
2025-09-02 10:29:58 -07:00
|
|
|
|
lines.push("".into());
|
2025-08-22 16:32:31 -07:00
|
|
|
|
}
|
2025-09-02 10:29:58 -07:00
|
|
|
|
lines
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-22 16:32:31 -07:00
|
|
|
|
|
2025-09-02 10:29:58 -07:00
|
|
|
|
impl ExecCell {
|
|
|
|
|
|
fn is_active(&self) -> bool {
|
|
|
|
|
|
self.calls.iter().any(|c| c.output.is_none())
|
|
|
|
|
|
}
|
2025-08-22 16:32:31 -07:00
|
|
|
|
|
2025-09-02 10:29:58 -07:00
|
|
|
|
fn exploring_display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
2025-09-05 07:10:32 -07:00
|
|
|
|
let mut out: Vec<Line<'static>> = Vec::new();
|
2025-09-02 10:29:58 -07:00
|
|
|
|
let active_start_time = self
|
|
|
|
|
|
.calls
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|c| c.output.is_none())
|
|
|
|
|
|
.and_then(|c| c.start_time);
|
2025-09-05 07:10:32 -07:00
|
|
|
|
out.push(Line::from(vec![
|
2025-09-02 10:29:58 -07:00
|
|
|
|
if self.is_active() {
|
|
|
|
|
|
// Show an animated spinner while exploring
|
|
|
|
|
|
spinner(active_start_time)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"•".bold()
|
|
|
|
|
|
},
|
|
|
|
|
|
" ".into(),
|
|
|
|
|
|
if self.is_active() {
|
|
|
|
|
|
"Exploring".bold()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"Explored".bold()
|
|
|
|
|
|
},
|
|
|
|
|
|
]));
|
|
|
|
|
|
let mut calls = self.calls.clone();
|
2025-09-05 07:10:32 -07:00
|
|
|
|
let mut out_indented = Vec::new();
|
2025-09-02 10:29:58 -07:00
|
|
|
|
while !calls.is_empty() {
|
|
|
|
|
|
let mut call = calls.remove(0);
|
|
|
|
|
|
if call
|
|
|
|
|
|
.parsed
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.all(|c| matches!(c, ParsedCommand::Read { .. }))
|
|
|
|
|
|
{
|
|
|
|
|
|
while let Some(next) = calls.first() {
|
|
|
|
|
|
if next
|
|
|
|
|
|
.parsed
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.all(|c| matches!(c, ParsedCommand::Read { .. }))
|
|
|
|
|
|
{
|
|
|
|
|
|
call.parsed.extend(next.parsed.clone());
|
|
|
|
|
|
calls.remove(0);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let call_lines: Vec<(&str, Vec<Span<'static>>)> = if call
|
|
|
|
|
|
.parsed
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.all(|c| matches!(c, ParsedCommand::Read { .. }))
|
|
|
|
|
|
{
|
|
|
|
|
|
let names: Vec<String> = call
|
|
|
|
|
|
.parsed
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|c| match c {
|
|
|
|
|
|
ParsedCommand::Read { name, .. } => name.clone(),
|
|
|
|
|
|
_ => unreachable!(),
|
|
|
|
|
|
})
|
|
|
|
|
|
.unique()
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
vec![(
|
|
|
|
|
|
"Read",
|
|
|
|
|
|
itertools::Itertools::intersperse(
|
|
|
|
|
|
names.into_iter().map(|n| n.into()),
|
|
|
|
|
|
", ".dim(),
|
|
|
|
|
|
)
|
|
|
|
|
|
.collect(),
|
|
|
|
|
|
)]
|
2025-08-22 16:32:31 -07:00
|
|
|
|
} else {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
let mut lines = Vec::new();
|
|
|
|
|
|
for p in call.parsed {
|
|
|
|
|
|
match p {
|
|
|
|
|
|
ParsedCommand::Read { name, .. } => {
|
|
|
|
|
|
lines.push(("Read", vec![name.into()]));
|
|
|
|
|
|
}
|
|
|
|
|
|
ParsedCommand::ListFiles { cmd, path } => {
|
|
|
|
|
|
lines.push(("List", vec![path.unwrap_or(cmd).into()]));
|
|
|
|
|
|
}
|
|
|
|
|
|
ParsedCommand::Search { cmd, query, path } => {
|
|
|
|
|
|
lines.push((
|
|
|
|
|
|
"Search",
|
|
|
|
|
|
match (query, path) {
|
|
|
|
|
|
(Some(q), Some(p)) => {
|
|
|
|
|
|
vec![q.into(), " in ".dim(), p.into()]
|
|
|
|
|
|
}
|
|
|
|
|
|
(Some(q), None) => vec![q.into()],
|
|
|
|
|
|
_ => vec![cmd.into()],
|
|
|
|
|
|
},
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
ParsedCommand::Unknown { cmd } => {
|
|
|
|
|
|
lines.push(("Run", vec![cmd.into()]));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
lines
|
2025-08-22 16:32:31 -07:00
|
|
|
|
};
|
2025-09-02 10:29:58 -07:00
|
|
|
|
for (title, line) in call_lines {
|
2025-09-05 07:10:32 -07:00
|
|
|
|
let line = Line::from(line);
|
|
|
|
|
|
let initial_indent = Line::from(vec![title.cyan(), " ".into()]);
|
|
|
|
|
|
let subsequent_indent = " ".repeat(initial_indent.width()).into();
|
|
|
|
|
|
let wrapped = word_wrap_line(
|
|
|
|
|
|
&line,
|
|
|
|
|
|
RtOptions::new(width as usize)
|
|
|
|
|
|
.initial_indent(initial_indent)
|
|
|
|
|
|
.subsequent_indent(subsequent_indent),
|
2025-09-02 10:29:58 -07:00
|
|
|
|
);
|
2025-09-05 07:10:32 -07:00
|
|
|
|
push_owned_lines(&wrapped, &mut out_indented);
|
2025-09-02 10:29:58 -07:00
|
|
|
|
}
|
2025-08-22 16:32:31 -07:00
|
|
|
|
}
|
2025-09-05 07:10:32 -07:00
|
|
|
|
out.extend(prefix_lines(out_indented, " └ ".dim(), " ".into()));
|
|
|
|
|
|
out
|
2025-09-02 10:29:58 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn command_display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
|
|
|
|
|
use textwrap::Options as TwOptions;
|
2025-08-22 16:32:31 -07:00
|
|
|
|
|
2025-09-02 10:29:58 -07:00
|
|
|
|
let mut lines: Vec<Line<'static>> = Vec::new();
|
|
|
|
|
|
let [call] = &self.calls.as_slice() else {
|
|
|
|
|
|
panic!("Expected exactly one call in a command display cell");
|
|
|
|
|
|
};
|
|
|
|
|
|
let success = call.output.as_ref().map(|o| o.exit_code == 0);
|
|
|
|
|
|
let bullet = match success {
|
|
|
|
|
|
Some(true) => "•".green().bold(),
|
|
|
|
|
|
Some(false) => "•".red().bold(),
|
|
|
|
|
|
None => spinner(call.start_time),
|
|
|
|
|
|
};
|
|
|
|
|
|
let title = if self.is_active() { "Running" } else { "Ran" };
|
|
|
|
|
|
let cmd_display = strip_bash_lc_and_escape(&call.command);
|
|
|
|
|
|
|
|
|
|
|
|
// If the command fits on the same line as the header at the current width,
|
|
|
|
|
|
// show a single compact line: "• Ran <command>". Use the width of
|
|
|
|
|
|
// "• Running " (including trailing space) as the reserved prefix width.
|
|
|
|
|
|
// If the command contains newlines, always use the multi-line variant.
|
|
|
|
|
|
let reserved = "• Running ".width();
|
|
|
|
|
|
|
2025-09-05 07:10:32 -07:00
|
|
|
|
let mut body_lines: Vec<Line<'static>> = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
let highlighted_lines = crate::render::highlight::highlight_bash_to_lines(&cmd_display);
|
|
|
|
|
|
|
|
|
|
|
|
if highlighted_lines.len() == 1
|
|
|
|
|
|
&& highlighted_lines[0].width() < (width as usize).saturating_sub(reserved)
|
2025-09-02 10:29:58 -07:00
|
|
|
|
{
|
2025-09-05 07:10:32 -07:00
|
|
|
|
let mut line = Line::from(vec![bullet, " ".into(), title.bold(), " ".into()]);
|
|
|
|
|
|
line.extend(highlighted_lines[0].clone());
|
|
|
|
|
|
lines.push(line);
|
2025-09-02 10:29:58 -07:00
|
|
|
|
} else {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![bullet, " ".into(), title.bold()].into());
|
2025-09-02 10:29:58 -07:00
|
|
|
|
|
2025-09-05 07:10:32 -07:00
|
|
|
|
for hl_line in highlighted_lines.iter() {
|
|
|
|
|
|
let opts = crate::wrapping::RtOptions::new((width as usize).saturating_sub(4))
|
|
|
|
|
|
.initial_indent("".into())
|
|
|
|
|
|
.subsequent_indent(" ".into())
|
|
|
|
|
|
// Hyphenation likes to break words on hyphens, which is bad for bash scripts --because-of-flags.
|
|
|
|
|
|
.word_splitter(textwrap::WordSplitter::NoHyphenation);
|
|
|
|
|
|
let wrapped_borrowed = crate::wrapping::word_wrap_line(hl_line, opts);
|
|
|
|
|
|
body_lines.extend(wrapped_borrowed.iter().map(|l| line_to_static(l)));
|
2025-09-02 10:29:58 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(output) = call.output.as_ref()
|
|
|
|
|
|
&& output.exit_code != 0
|
|
|
|
|
|
{
|
|
|
|
|
|
let out = output_lines(Some(output), false, false, false)
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.join("\n");
|
|
|
|
|
|
if !out.trim().is_empty() {
|
|
|
|
|
|
// Wrap the output.
|
2025-09-05 07:10:32 -07:00
|
|
|
|
for line in out.lines() {
|
|
|
|
|
|
let wrapped = textwrap::wrap(line, TwOptions::new(width as usize - 4));
|
|
|
|
|
|
body_lines.extend(wrapped.into_iter().map(|l| Line::from(l.to_string().dim())));
|
2025-09-02 10:29:58 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-05 07:10:32 -07:00
|
|
|
|
lines.extend(prefix_lines(body_lines, " └ ".dim(), " ".into()));
|
2025-08-22 16:32:31 -07:00
|
|
|
|
lines
|
|
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}
|
2025-08-11 12:40:12 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
impl WidgetRef for &ExecCell {
|
|
|
|
|
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
2025-08-22 16:32:31 -07:00
|
|
|
|
if area.height == 0 {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let content_area = Rect {
|
|
|
|
|
|
x: area.x,
|
|
|
|
|
|
y: area.y,
|
|
|
|
|
|
width: area.width,
|
|
|
|
|
|
height: area.height,
|
|
|
|
|
|
};
|
2025-09-02 10:29:58 -07:00
|
|
|
|
let lines = self.display_lines(area.width);
|
|
|
|
|
|
let max_rows = area.height as usize;
|
|
|
|
|
|
let rendered = if lines.len() > max_rows {
|
|
|
|
|
|
// Keep the last `max_rows` lines in original order
|
|
|
|
|
|
lines[lines.len() - max_rows..].to_vec()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
lines
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Paragraph::new(Text::from(rendered))
|
2025-08-14 14:10:05 -04:00
|
|
|
|
.wrap(Wrap { trim: false })
|
2025-08-22 16:32:31 -07:00
|
|
|
|
.render(content_area, buf);
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-25 20:15:38 -07:00
|
|
|
|
impl ExecCell {
|
|
|
|
|
|
/// Convert an active exec cell into a failed, completed exec cell.
|
2025-09-02 10:29:58 -07:00
|
|
|
|
/// Any call without output is marked as failed with a red ✗.
|
2025-08-25 20:15:38 -07:00
|
|
|
|
pub(crate) fn into_failed(mut self) -> ExecCell {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
for call in self.calls.iter_mut() {
|
|
|
|
|
|
if call.output.is_none() {
|
|
|
|
|
|
let elapsed = call
|
|
|
|
|
|
.start_time
|
|
|
|
|
|
.map(|st| st.elapsed())
|
|
|
|
|
|
.unwrap_or_else(|| Duration::from_millis(0));
|
|
|
|
|
|
call.start_time = None;
|
|
|
|
|
|
call.duration = Some(elapsed);
|
|
|
|
|
|
call.output = Some(CommandOutput {
|
|
|
|
|
|
exit_code: 1,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr: String::new(),
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-25 20:15:38 -07:00
|
|
|
|
self
|
|
|
|
|
|
}
|
2025-09-02 10:29:58 -07:00
|
|
|
|
|
|
|
|
|
|
pub(crate) fn new(call: ExecCall) -> Self {
|
|
|
|
|
|
ExecCell { calls: vec![call] }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn is_exploring_call(call: &ExecCall) -> bool {
|
|
|
|
|
|
!call.parsed.is_empty()
|
|
|
|
|
|
&& call.parsed.iter().all(|p| {
|
|
|
|
|
|
matches!(
|
|
|
|
|
|
p,
|
|
|
|
|
|
ParsedCommand::Read { .. }
|
|
|
|
|
|
| ParsedCommand::ListFiles { .. }
|
|
|
|
|
|
| ParsedCommand::Search { .. }
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn is_exploring_cell(&self) -> bool {
|
|
|
|
|
|
self.calls.iter().all(Self::is_exploring_call)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(crate) fn with_added_call(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
call_id: String,
|
|
|
|
|
|
command: Vec<String>,
|
|
|
|
|
|
parsed: Vec<ParsedCommand>,
|
|
|
|
|
|
) -> Option<Self> {
|
|
|
|
|
|
let call = ExecCall {
|
|
|
|
|
|
call_id,
|
|
|
|
|
|
command,
|
|
|
|
|
|
parsed,
|
|
|
|
|
|
output: None,
|
|
|
|
|
|
start_time: Some(Instant::now()),
|
|
|
|
|
|
duration: None,
|
|
|
|
|
|
};
|
|
|
|
|
|
if self.is_exploring_cell() && Self::is_exploring_call(&call) {
|
|
|
|
|
|
Some(Self {
|
|
|
|
|
|
calls: [self.calls.clone(), vec![call]].concat(),
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(crate) fn complete_call(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
call_id: &str,
|
|
|
|
|
|
output: CommandOutput,
|
|
|
|
|
|
duration: Duration,
|
|
|
|
|
|
) {
|
|
|
|
|
|
if let Some(call) = self.calls.iter_mut().rev().find(|c| c.call_id == call_id) {
|
|
|
|
|
|
call.output = Some(output);
|
|
|
|
|
|
call.duration = Some(duration);
|
|
|
|
|
|
call.start_time = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(crate) fn should_flush(&self) -> bool {
|
|
|
|
|
|
!self.is_exploring_cell() && self.calls.iter().all(|c| c.output.is_some())
|
|
|
|
|
|
}
|
2025-08-25 20:15:38 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-20 17:09:46 -07:00
|
|
|
|
#[derive(Debug)]
|
2025-08-14 14:10:05 -04:00
|
|
|
|
struct CompletedMcpToolCallWithImageOutput {
|
|
|
|
|
|
_image: DynamicImage,
|
|
|
|
|
|
}
|
|
|
|
|
|
impl HistoryCell for CompletedMcpToolCallWithImageOutput {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
fn display_lines(&self, _width: u16) -> Vec<Line<'static>> {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
vec!["tool result (image output omitted)".into()]
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-10 21:32:56 -07:00
|
|
|
|
const TOOL_CALL_MAX_LINES: usize = 5;
|
2025-05-06 16:12:15 -07:00
|
|
|
|
|
2025-08-07 04:02:58 -07:00
|
|
|
|
fn title_case(s: &str) -> String {
|
|
|
|
|
|
if s.is_empty() {
|
|
|
|
|
|
return String::new();
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut chars = s.chars();
|
|
|
|
|
|
let first = match chars.next() {
|
|
|
|
|
|
Some(c) => c,
|
|
|
|
|
|
None => return String::new(),
|
|
|
|
|
|
};
|
|
|
|
|
|
let rest: String = chars.as_str().to_ascii_lowercase();
|
|
|
|
|
|
first.to_uppercase().collect::<String>() + &rest
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn pretty_provider_name(id: &str) -> String {
|
|
|
|
|
|
if id.eq_ignore_ascii_case("openai") {
|
|
|
|
|
|
"OpenAI".to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
title_case(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-26 17:34:24 -07:00
|
|
|
|
/// Return the emoji followed by a hair space (U+200A).
|
|
|
|
|
|
/// Using only the hair space avoids excessive padding after the emoji while
|
|
|
|
|
|
/// still providing a small visual gap across terminals.
|
2025-08-25 22:49:19 -07:00
|
|
|
|
fn padded_emoji(emoji: &str) -> String {
|
2025-08-26 17:34:24 -07:00
|
|
|
|
format!("{emoji}\u{200A}")
|
2025-08-25 22:49:19 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
pub(crate) fn new_session_info(
|
|
|
|
|
|
config: &Config,
|
|
|
|
|
|
event: SessionConfiguredEvent,
|
|
|
|
|
|
is_first_event: bool,
|
|
|
|
|
|
) -> PlainHistoryCell {
|
|
|
|
|
|
let SessionConfiguredEvent {
|
|
|
|
|
|
model,
|
|
|
|
|
|
session_id: _,
|
|
|
|
|
|
history_log_id: _,
|
|
|
|
|
|
history_entry_count: _,
|
2025-09-03 21:47:00 -07:00
|
|
|
|
initial_messages: _,
|
2025-09-09 00:11:48 -07:00
|
|
|
|
rollout_path: _,
|
2025-08-14 14:10:05 -04:00
|
|
|
|
} = event;
|
|
|
|
|
|
if is_first_event {
|
|
|
|
|
|
let cwd_str = match relativize_to_home(&config.cwd) {
|
2025-08-25 14:47:17 -07:00
|
|
|
|
Some(rel) if !rel.as_os_str().is_empty() => {
|
|
|
|
|
|
let sep = std::path::MAIN_SEPARATOR;
|
|
|
|
|
|
format!("~{sep}{}", rel.display())
|
|
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
Some(_) => "~".to_string(),
|
|
|
|
|
|
None => config.cwd.display().to_string(),
|
|
|
|
|
|
};
|
2025-09-02 12:04:32 -07:00
|
|
|
|
// Discover AGENTS.md files to decide whether to suggest `/init`.
|
|
|
|
|
|
let has_agents_md = discover_project_doc_paths(config)
|
|
|
|
|
|
.map(|v| !v.is_empty())
|
|
|
|
|
|
.unwrap_or(false);
|
2025-08-12 17:37:28 -07:00
|
|
|
|
|
2025-09-02 12:04:32 -07:00
|
|
|
|
let mut lines: Vec<Line<'static>> = Vec::new();
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
">_ ".dim(),
|
|
|
|
|
|
"You are using OpenAI Codex in".bold(),
|
|
|
|
|
|
format!(" {cwd_str}").dim(),
|
|
|
|
|
|
]));
|
|
|
|
|
|
lines.push(Line::from("".dim()));
|
|
|
|
|
|
lines.push(Line::from(
|
|
|
|
|
|
" To get started, describe a task or try one of these commands:".dim(),
|
|
|
|
|
|
));
|
|
|
|
|
|
lines.push(Line::from("".dim()));
|
|
|
|
|
|
if !has_agents_md {
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
" /init".bold(),
|
|
|
|
|
|
format!(" - {}", SlashCommand::Init.description()).dim(),
|
|
|
|
|
|
]));
|
|
|
|
|
|
}
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
" /status".bold(),
|
|
|
|
|
|
format!(" - {}", SlashCommand::Status.description()).dim(),
|
|
|
|
|
|
]));
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
" /approvals".bold(),
|
|
|
|
|
|
format!(" - {}", SlashCommand::Approvals.description()).dim(),
|
|
|
|
|
|
]));
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
" /model".bold(),
|
|
|
|
|
|
format!(" - {}", SlashCommand::Model.description()).dim(),
|
|
|
|
|
|
]));
|
2025-08-14 14:10:05 -04:00
|
|
|
|
PlainHistoryCell { lines }
|
|
|
|
|
|
} else if config.model == model {
|
|
|
|
|
|
PlainHistoryCell { lines: Vec::new() }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let lines = vec![
|
2025-09-02 16:19:54 -07:00
|
|
|
|
"model changed:".magenta().bold().into(),
|
|
|
|
|
|
format!("requested: {}", config.model).into(),
|
|
|
|
|
|
format!("used: {model}").into(),
|
2025-08-14 14:10:05 -04:00
|
|
|
|
];
|
|
|
|
|
|
PlainHistoryCell { lines }
|
2025-08-06 12:03:45 -07:00
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}
|
2025-08-06 12:03:45 -07:00
|
|
|
|
|
2025-09-02 10:29:58 -07:00
|
|
|
|
pub(crate) fn new_user_prompt(message: String) -> UserHistoryCell {
|
|
|
|
|
|
UserHistoryCell { message }
|
|
|
|
|
|
}
|
2025-05-30 23:24:36 -07:00
|
|
|
|
|
2025-09-02 10:29:58 -07:00
|
|
|
|
pub(crate) fn new_user_approval_decision(lines: Vec<Line<'static>>) -> PlainHistoryCell {
|
2025-08-14 14:10:05 -04:00
|
|
|
|
PlainHistoryCell { lines }
|
|
|
|
|
|
}
|
2025-05-08 21:46:06 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
pub(crate) fn new_active_exec_command(
|
2025-09-02 10:29:58 -07:00
|
|
|
|
call_id: String,
|
2025-08-14 14:10:05 -04:00
|
|
|
|
command: Vec<String>,
|
|
|
|
|
|
parsed: Vec<ParsedCommand>,
|
|
|
|
|
|
) -> ExecCell {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
ExecCell::new(ExecCall {
|
|
|
|
|
|
call_id,
|
2025-08-14 19:32:45 -04:00
|
|
|
|
command,
|
|
|
|
|
|
parsed,
|
|
|
|
|
|
output: None,
|
|
|
|
|
|
start_time: Some(Instant::now()),
|
2025-08-22 16:32:31 -07:00
|
|
|
|
duration: None,
|
2025-09-02 10:29:58 -07:00
|
|
|
|
})
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
|
2025-09-02 10:29:58 -07:00
|
|
|
|
fn spinner(start_time: Option<Instant>) -> Span<'static> {
|
|
|
|
|
|
const FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
|
|
|
|
let idx = start_time
|
|
|
|
|
|
.map(|st| ((st.elapsed().as_millis() / 100) as usize) % FRAMES.len())
|
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
let ch = FRAMES[idx];
|
|
|
|
|
|
ch.to_string().into()
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
pub(crate) fn new_active_mcp_tool_call(invocation: McpInvocation) -> PlainHistoryCell {
|
|
|
|
|
|
let title_line = Line::from(vec!["tool".magenta(), " running...".dim()]);
|
2025-09-11 11:59:37 -07:00
|
|
|
|
let lines: Vec<Line> = vec![title_line, format_mcp_invocation(invocation)];
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
PlainHistoryCell { lines }
|
|
|
|
|
|
}
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
|
2025-08-23 22:58:56 -07:00
|
|
|
|
pub(crate) fn new_web_search_call(query: String) -> PlainHistoryCell {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
let lines: Vec<Line<'static>> = vec![Line::from(vec![padded_emoji("🌐").into(), query.into()])];
|
2025-08-23 22:58:56 -07:00
|
|
|
|
PlainHistoryCell { lines }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
/// If the first content is an image, return a new cell with the image.
|
|
|
|
|
|
/// TODO(rgwood-dd): Handle images properly even if they're not the first result.
|
|
|
|
|
|
fn try_new_completed_mcp_tool_call_with_image_output(
|
|
|
|
|
|
result: &Result<mcp_types::CallToolResult, String>,
|
|
|
|
|
|
) -> Option<CompletedMcpToolCallWithImageOutput> {
|
|
|
|
|
|
match result {
|
|
|
|
|
|
Ok(mcp_types::CallToolResult { content, .. }) => {
|
|
|
|
|
|
if let Some(mcp_types::ContentBlock::ImageContent(image)) = content.first() {
|
|
|
|
|
|
let raw_data = match base64::engine::general_purpose::STANDARD.decode(&image.data) {
|
|
|
|
|
|
Ok(data) => data,
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
error!("Failed to decode image data: {e}");
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
let reader = match ImageReader::new(Cursor::new(raw_data)).with_guessed_format() {
|
|
|
|
|
|
Ok(reader) => reader,
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
error!("Failed to guess image format: {e}");
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
let image = match reader.decode() {
|
|
|
|
|
|
Ok(image) => image,
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
error!("Image decoding failed: {e}");
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-05-06 16:12:15 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
Some(CompletedMcpToolCallWithImageOutput { _image: image })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
2025-05-06 16:12:15 -07:00
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
_ => None,
|
2025-05-06 16:12:15 -07:00
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}
|
2025-05-06 16:12:15 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
pub(crate) fn new_completed_mcp_tool_call(
|
|
|
|
|
|
num_cols: usize,
|
|
|
|
|
|
invocation: McpInvocation,
|
|
|
|
|
|
duration: Duration,
|
|
|
|
|
|
success: bool,
|
|
|
|
|
|
result: Result<mcp_types::CallToolResult, String>,
|
|
|
|
|
|
) -> Box<dyn HistoryCell> {
|
|
|
|
|
|
if let Some(cell) = try_new_completed_mcp_tool_call_with_image_output(&result) {
|
|
|
|
|
|
return Box::new(cell);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let duration = format_duration(duration);
|
|
|
|
|
|
let status_str = if success { "success" } else { "failed" };
|
|
|
|
|
|
let title_line = Line::from(vec![
|
|
|
|
|
|
"tool".magenta(),
|
|
|
|
|
|
" ".into(),
|
|
|
|
|
|
if success {
|
|
|
|
|
|
status_str.green()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
status_str.red()
|
|
|
|
|
|
},
|
|
|
|
|
|
format!(", duration: {duration}").dim(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
let mut lines: Vec<Line<'static>> = Vec::new();
|
|
|
|
|
|
lines.push(title_line);
|
|
|
|
|
|
lines.push(format_mcp_invocation(invocation));
|
|
|
|
|
|
|
|
|
|
|
|
match result {
|
|
|
|
|
|
Ok(mcp_types::CallToolResult { content, .. }) => {
|
|
|
|
|
|
if !content.is_empty() {
|
|
|
|
|
|
lines.push(Line::from(""));
|
fix: introduce ResponseInputItem::McpToolCallOutput variant (#1151)
The output of an MCP server tool call can be one of several types, but
to date, we treated all outputs as text by showing the serialized JSON
as the "tool output" in Codex:
https://github.com/openai/codex/blob/25a9949c49194d5a64de54a11bcc5b4724ac9bd5/codex-rs/mcp-types/src/lib.rs#L96-L101
This PR adds support for the `ImageContent` variant so we can now
display an image output from an MCP tool call.
In making this change, we introduce a new
`ResponseInputItem::McpToolCallOutput` variant so that we can work with
the `mcp_types::CallToolResult` directly when the function call is made
to an MCP server.
Though arguably the more significant change is the introduction of
`HistoryCell::CompletedMcpToolCallWithImageOutput`, which is a cell that
uses `ratatui_image` to render an image into the terminal. To support
this, we introduce `ImageRenderCache`, cache a
`ratatui_image::picker::Picker`, and `ensure_image_cache()` to cache the
appropriate scaled image data and dimensions based on the current
terminal size.
To test, I created a minimal `package.json`:
```json
{
"name": "kitty-mcp",
"version": "1.0.0",
"type": "module",
"description": "MCP that returns image of kitty",
"main": "index.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0"
}
}
```
with the following `index.js` to define the MCP server:
```js
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
const IMAGE_URI = "image://Ada.png";
const server = new McpServer({
name: "Demo",
version: "1.0.0",
});
server.tool(
"get-cat-image",
"If you need a cat image, this tool will provide one.",
async () => ({
content: [
{ type: "image", data: await getAdaPngBase64(), mimeType: "image/png" },
],
})
);
server.resource("Ada the Cat", IMAGE_URI, async (uri) => {
const base64Image = await getAdaPngBase64();
return {
contents: [
{
uri: uri.href,
mimeType: "image/png",
blob: base64Image,
},
],
};
});
async function getAdaPngBase64() {
const __dirname = new URL(".", import.meta.url).pathname;
// From https://github.com/benjajaja/ratatui-image/blob/9705ce2c59ec669abbce2924cbfd1f5ae22c9860/assets/Ada.png
const filePath = join(__dirname, "Ada.png");
const imageData = await readFile(filePath);
const base64Image = imageData.toString("base64");
return base64Image;
}
const transport = new StdioServerTransport();
await server.connect(transport);
```
With the local changes from this PR, I added the following to my
`config.toml`:
```toml
[mcp_servers.kitty]
command = "node"
args = ["/Users/mbolin/code/kitty-mcp/index.js"]
```
Running the TUI from source:
```
cargo run --bin codex -- --model o3 'I need a picture of a cat'
```
I get:
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/bf80b721-9ca0-4d81-aec7-77d6899e2869"
/>
Now, that said, I have only tested in iTerm and there is definitely some
funny business with getting an accurate character-to-pixel ratio
(sometimes the `CompletedMcpToolCallWithImageOutput` thinks it needs 10
rows to render instead of 4), so there is still work to be done here.
2025-05-28 19:03:17 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
for tool_call_result in content {
|
|
|
|
|
|
let line_text = match tool_call_result {
|
|
|
|
|
|
mcp_types::ContentBlock::TextContent(text) => {
|
|
|
|
|
|
format_and_truncate_tool_result(
|
|
|
|
|
|
&text.text,
|
|
|
|
|
|
TOOL_CALL_MAX_LINES,
|
|
|
|
|
|
num_cols,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
mcp_types::ContentBlock::ImageContent(_) => {
|
|
|
|
|
|
// TODO show images even if they're not the first result, will require a refactor of `CompletedMcpToolCall`
|
|
|
|
|
|
"<image content>".to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
mcp_types::ContentBlock::AudioContent(_) => "<audio content>".to_string(),
|
|
|
|
|
|
mcp_types::ContentBlock::EmbeddedResource(resource) => {
|
|
|
|
|
|
let uri = match resource.resource {
|
|
|
|
|
|
EmbeddedResourceResource::TextResourceContents(text) => text.uri,
|
|
|
|
|
|
EmbeddedResourceResource::BlobResourceContents(blob) => blob.uri,
|
|
|
|
|
|
};
|
|
|
|
|
|
format!("embedded resource: {uri}")
|
|
|
|
|
|
}
|
|
|
|
|
|
mcp_types::ContentBlock::ResourceLink(ResourceLink { uri, .. }) => {
|
|
|
|
|
|
format!("link: {uri}")
|
fix: introduce ResponseInputItem::McpToolCallOutput variant (#1151)
The output of an MCP server tool call can be one of several types, but
to date, we treated all outputs as text by showing the serialized JSON
as the "tool output" in Codex:
https://github.com/openai/codex/blob/25a9949c49194d5a64de54a11bcc5b4724ac9bd5/codex-rs/mcp-types/src/lib.rs#L96-L101
This PR adds support for the `ImageContent` variant so we can now
display an image output from an MCP tool call.
In making this change, we introduce a new
`ResponseInputItem::McpToolCallOutput` variant so that we can work with
the `mcp_types::CallToolResult` directly when the function call is made
to an MCP server.
Though arguably the more significant change is the introduction of
`HistoryCell::CompletedMcpToolCallWithImageOutput`, which is a cell that
uses `ratatui_image` to render an image into the terminal. To support
this, we introduce `ImageRenderCache`, cache a
`ratatui_image::picker::Picker`, and `ensure_image_cache()` to cache the
appropriate scaled image data and dimensions based on the current
terminal size.
To test, I created a minimal `package.json`:
```json
{
"name": "kitty-mcp",
"version": "1.0.0",
"type": "module",
"description": "MCP that returns image of kitty",
"main": "index.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0"
}
}
```
with the following `index.js` to define the MCP server:
```js
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
const IMAGE_URI = "image://Ada.png";
const server = new McpServer({
name: "Demo",
version: "1.0.0",
});
server.tool(
"get-cat-image",
"If you need a cat image, this tool will provide one.",
async () => ({
content: [
{ type: "image", data: await getAdaPngBase64(), mimeType: "image/png" },
],
})
);
server.resource("Ada the Cat", IMAGE_URI, async (uri) => {
const base64Image = await getAdaPngBase64();
return {
contents: [
{
uri: uri.href,
mimeType: "image/png",
blob: base64Image,
},
],
};
});
async function getAdaPngBase64() {
const __dirname = new URL(".", import.meta.url).pathname;
// From https://github.com/benjajaja/ratatui-image/blob/9705ce2c59ec669abbce2924cbfd1f5ae22c9860/assets/Ada.png
const filePath = join(__dirname, "Ada.png");
const imageData = await readFile(filePath);
const base64Image = imageData.toString("base64");
return base64Image;
}
const transport = new StdioServerTransport();
await server.connect(transport);
```
With the local changes from this PR, I added the following to my
`config.toml`:
```toml
[mcp_servers.kitty]
command = "node"
args = ["/Users/mbolin/code/kitty-mcp/index.js"]
```
Running the TUI from source:
```
cargo run --bin codex -- --model o3 'I need a picture of a cat'
```
I get:
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/bf80b721-9ca0-4d81-aec7-77d6899e2869"
/>
Now, that said, I have only tested in iTerm and there is definitely some
funny business with getting an accurate character-to-pixel ratio
(sometimes the `CompletedMcpToolCallWithImageOutput` thinks it needs 10
rows to render instead of 4), so there is still work to be done here.
2025-05-28 19:03:17 -07:00
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-08-14 14:10:05 -04:00
|
|
|
|
lines.push(Line::styled(
|
|
|
|
|
|
line_text,
|
|
|
|
|
|
Style::default().add_modifier(Modifier::DIM),
|
|
|
|
|
|
));
|
fix: introduce ResponseInputItem::McpToolCallOutput variant (#1151)
The output of an MCP server tool call can be one of several types, but
to date, we treated all outputs as text by showing the serialized JSON
as the "tool output" in Codex:
https://github.com/openai/codex/blob/25a9949c49194d5a64de54a11bcc5b4724ac9bd5/codex-rs/mcp-types/src/lib.rs#L96-L101
This PR adds support for the `ImageContent` variant so we can now
display an image output from an MCP tool call.
In making this change, we introduce a new
`ResponseInputItem::McpToolCallOutput` variant so that we can work with
the `mcp_types::CallToolResult` directly when the function call is made
to an MCP server.
Though arguably the more significant change is the introduction of
`HistoryCell::CompletedMcpToolCallWithImageOutput`, which is a cell that
uses `ratatui_image` to render an image into the terminal. To support
this, we introduce `ImageRenderCache`, cache a
`ratatui_image::picker::Picker`, and `ensure_image_cache()` to cache the
appropriate scaled image data and dimensions based on the current
terminal size.
To test, I created a minimal `package.json`:
```json
{
"name": "kitty-mcp",
"version": "1.0.0",
"type": "module",
"description": "MCP that returns image of kitty",
"main": "index.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0"
}
}
```
with the following `index.js` to define the MCP server:
```js
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
const IMAGE_URI = "image://Ada.png";
const server = new McpServer({
name: "Demo",
version: "1.0.0",
});
server.tool(
"get-cat-image",
"If you need a cat image, this tool will provide one.",
async () => ({
content: [
{ type: "image", data: await getAdaPngBase64(), mimeType: "image/png" },
],
})
);
server.resource("Ada the Cat", IMAGE_URI, async (uri) => {
const base64Image = await getAdaPngBase64();
return {
contents: [
{
uri: uri.href,
mimeType: "image/png",
blob: base64Image,
},
],
};
});
async function getAdaPngBase64() {
const __dirname = new URL(".", import.meta.url).pathname;
// From https://github.com/benjajaja/ratatui-image/blob/9705ce2c59ec669abbce2924cbfd1f5ae22c9860/assets/Ada.png
const filePath = join(__dirname, "Ada.png");
const imageData = await readFile(filePath);
const base64Image = imageData.toString("base64");
return base64Image;
}
const transport = new StdioServerTransport();
await server.connect(transport);
```
With the local changes from this PR, I added the following to my
`config.toml`:
```toml
[mcp_servers.kitty]
command = "node"
args = ["/Users/mbolin/code/kitty-mcp/index.js"]
```
Running the TUI from source:
```
cargo run --bin codex -- --model o3 'I need a picture of a cat'
```
I get:
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/bf80b721-9ca0-4d81-aec7-77d6899e2869"
/>
Now, that said, I have only tested in iTerm and there is definitely some
funny business with getting an accurate character-to-pixel ratio
(sometimes the `CompletedMcpToolCallWithImageOutput` thinks it needs 10
rows to render instead of 4), so there is still work to be done here.
2025-05-28 19:03:17 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
Err(e) => {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec!["Error: ".red().bold(), e.into()].into());
|
2025-05-28 14:03:19 -07:00
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
};
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
Box::new(PlainHistoryCell { lines })
|
|
|
|
|
|
}
|
2025-06-26 13:03:31 -07:00
|
|
|
|
|
2025-08-14 19:14:46 -07:00
|
|
|
|
pub(crate) fn new_status_output(
|
|
|
|
|
|
config: &Config,
|
|
|
|
|
|
usage: &TokenUsage,
|
2025-09-07 20:22:25 -07:00
|
|
|
|
session_id: &Option<ConversationId>,
|
2025-08-14 19:14:46 -07:00
|
|
|
|
) -> PlainHistoryCell {
|
2025-08-14 14:10:05 -04:00
|
|
|
|
let mut lines: Vec<Line<'static>> = Vec::new();
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push("/status".magenta().into());
|
2025-08-05 23:57:52 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
let config_entries = create_config_summary_entries(config);
|
|
|
|
|
|
let lookup = |k: &str| -> String {
|
|
|
|
|
|
config_entries
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|(key, _)| *key == k)
|
|
|
|
|
|
.map(|(_, v)| v.clone())
|
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
|
};
|
2025-08-07 01:27:45 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
// 📂 Workspace
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![padded_emoji("📂").into(), "Workspace".bold()].into());
|
2025-08-14 14:10:05 -04:00
|
|
|
|
// Path (home-relative, e.g., ~/code/project)
|
|
|
|
|
|
let cwd_str = match relativize_to_home(&config.cwd) {
|
2025-08-25 14:47:17 -07:00
|
|
|
|
Some(rel) if !rel.as_os_str().is_empty() => {
|
|
|
|
|
|
let sep = std::path::MAIN_SEPARATOR;
|
|
|
|
|
|
format!("~{sep}{}", rel.display())
|
|
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
Some(_) => "~".to_string(),
|
|
|
|
|
|
None => config.cwd.display().to_string(),
|
|
|
|
|
|
};
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • Path: ".into(), cwd_str.into()].into());
|
2025-08-14 14:10:05 -04:00
|
|
|
|
// Approval mode (as-is)
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • Approval Mode: ".into(), lookup("approval").into()].into());
|
2025-08-14 14:10:05 -04:00
|
|
|
|
// Sandbox (simplified name only)
|
|
|
|
|
|
let sandbox_name = match &config.sandbox_policy {
|
|
|
|
|
|
SandboxPolicy::DangerFullAccess => "danger-full-access",
|
|
|
|
|
|
SandboxPolicy::ReadOnly => "read-only",
|
|
|
|
|
|
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write",
|
|
|
|
|
|
};
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • Sandbox: ".into(), sandbox_name.into()].into());
|
2025-08-14 14:10:05 -04:00
|
|
|
|
|
2025-08-21 08:52:17 -07:00
|
|
|
|
// AGENTS.md files discovered via core's project_doc logic
|
|
|
|
|
|
let agents_list = {
|
|
|
|
|
|
match discover_project_doc_paths(config) {
|
|
|
|
|
|
Ok(paths) => {
|
|
|
|
|
|
let mut rels: Vec<String> = Vec::new();
|
|
|
|
|
|
for p in paths {
|
|
|
|
|
|
let display = if let Some(parent) = p.parent() {
|
|
|
|
|
|
if parent == config.cwd {
|
|
|
|
|
|
"AGENTS.md".to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let mut cur = config.cwd.as_path();
|
|
|
|
|
|
let mut ups = 0usize;
|
|
|
|
|
|
let mut reached = false;
|
|
|
|
|
|
while let Some(c) = cur.parent() {
|
|
|
|
|
|
if cur == parent {
|
|
|
|
|
|
reached = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
cur = c;
|
|
|
|
|
|
ups += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if reached {
|
2025-08-25 14:47:17 -07:00
|
|
|
|
let up = format!("..{}", std::path::MAIN_SEPARATOR);
|
|
|
|
|
|
format!("{}AGENTS.md", up.repeat(ups))
|
2025-08-21 08:52:17 -07:00
|
|
|
|
} else if let Ok(stripped) = p.strip_prefix(&config.cwd) {
|
|
|
|
|
|
stripped.display().to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
p.display().to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
p.display().to_string()
|
|
|
|
|
|
};
|
|
|
|
|
|
rels.push(display);
|
|
|
|
|
|
}
|
|
|
|
|
|
rels
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(_) => Vec::new(),
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
if agents_list.is_empty() {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(" • AGENTS files: (none)".into());
|
2025-08-21 08:52:17 -07:00
|
|
|
|
} else {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • AGENTS files: ".into(), agents_list.join(", ").into()].into());
|
2025-08-21 08:52:17 -07:00
|
|
|
|
}
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push("".into());
|
2025-08-21 08:52:17 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
// 👤 Account (only if ChatGPT tokens exist), shown under the first block
|
|
|
|
|
|
let auth_file = get_auth_file(&config.codex_home);
|
2025-08-19 13:22:02 -07:00
|
|
|
|
if let Ok(auth) = try_read_auth_json(&auth_file)
|
|
|
|
|
|
&& let Some(tokens) = auth.tokens.clone()
|
|
|
|
|
|
{
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![padded_emoji("👤").into(), "Account".bold()].into());
|
|
|
|
|
|
lines.push(" • Signed in with ChatGPT".into());
|
2025-08-07 01:27:45 -07:00
|
|
|
|
|
2025-08-19 13:22:02 -07:00
|
|
|
|
let info = tokens.id_token;
|
|
|
|
|
|
if let Some(email) = &info.email {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • Login: ".into(), email.clone().into()].into());
|
2025-08-19 13:22:02 -07:00
|
|
|
|
}
|
2025-08-07 01:27:45 -07:00
|
|
|
|
|
2025-08-19 13:22:02 -07:00
|
|
|
|
match auth.openai_api_key.as_deref() {
|
|
|
|
|
|
Some(key) if !key.is_empty() => {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(" • Using API key. Run codex login to use ChatGPT plan".into());
|
2025-08-19 13:22:02 -07:00
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
let plan_text = info
|
|
|
|
|
|
.get_chatgpt_plan_type()
|
|
|
|
|
|
.map(|s| title_case(&s))
|
|
|
|
|
|
.unwrap_or_else(|| "Unknown".to_string());
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • Plan: ".into(), plan_text.into()].into());
|
2025-08-19 13:22:02 -07:00
|
|
|
|
}
|
2025-08-07 04:02:58 -07:00
|
|
|
|
}
|
2025-08-19 13:22:02 -07:00
|
|
|
|
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push("".into());
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}
|
2025-08-07 04:02:58 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
// 🧠 Model
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![padded_emoji("🧠").into(), "Model".bold()].into());
|
|
|
|
|
|
lines.push(vec![" • Name: ".into(), config.model.clone().into()].into());
|
2025-08-14 14:10:05 -04:00
|
|
|
|
let provider_disp = pretty_provider_name(&config.model_provider_id);
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • Provider: ".into(), provider_disp.into()].into());
|
2025-08-14 14:10:05 -04:00
|
|
|
|
// Only show Reasoning fields if present in config summary
|
|
|
|
|
|
let reff = lookup("reasoning effort");
|
|
|
|
|
|
if !reff.is_empty() {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • Reasoning Effort: ".into(), title_case(&reff).into()].into());
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}
|
|
|
|
|
|
let rsum = lookup("reasoning summaries");
|
|
|
|
|
|
if !rsum.is_empty() {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • Reasoning Summaries: ".into(), title_case(&rsum).into()].into());
|
2025-08-05 23:57:52 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push("".into());
|
2025-08-07 03:55:59 -07:00
|
|
|
|
|
2025-09-05 16:27:31 -07:00
|
|
|
|
// 💻 Client
|
|
|
|
|
|
let cli_version = crate::version::CODEX_CLI_VERSION;
|
|
|
|
|
|
lines.push(vec![padded_emoji("💻").into(), "Client".bold()].into());
|
|
|
|
|
|
lines.push(vec![" • CLI Version: ".into(), cli_version.into()].into());
|
|
|
|
|
|
lines.push("".into());
|
|
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
// 📊 Token Usage
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec!["📊 ".into(), "Token Usage".bold()].into());
|
2025-08-19 11:27:05 -07:00
|
|
|
|
if let Some(session_id) = session_id {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • Session ID: ".into(), session_id.to_string().into()].into());
|
2025-08-19 11:27:05 -07:00
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
// Input: <input> [+ <cached> cached]
|
|
|
|
|
|
let mut input_line_spans: Vec<Span<'static>> = vec![
|
|
|
|
|
|
" • Input: ".into(),
|
2025-09-08 14:48:48 -07:00
|
|
|
|
format_with_separators(usage.non_cached_input()).into(),
|
2025-08-14 14:10:05 -04:00
|
|
|
|
];
|
2025-09-06 08:19:23 -07:00
|
|
|
|
if usage.cached_input_tokens > 0 {
|
|
|
|
|
|
let cached = usage.cached_input_tokens;
|
2025-08-19 13:22:02 -07:00
|
|
|
|
input_line_spans.push(format!(" (+ {cached} cached)").into());
|
2025-05-08 21:46:06 -07:00
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
lines.push(Line::from(input_line_spans));
|
|
|
|
|
|
// Output: <output>
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
" • Output: ".into(),
|
2025-09-08 14:48:48 -07:00
|
|
|
|
format_with_separators(usage.output_tokens).into(),
|
2025-08-14 14:10:05 -04:00
|
|
|
|
]));
|
|
|
|
|
|
// Total: <total>
|
|
|
|
|
|
lines.push(Line::from(vec![
|
|
|
|
|
|
" • Total: ".into(),
|
2025-09-08 14:48:48 -07:00
|
|
|
|
format_with_separators(usage.blended_total()).into(),
|
2025-08-14 14:10:05 -04:00
|
|
|
|
]));
|
|
|
|
|
|
|
|
|
|
|
|
PlainHistoryCell { lines }
|
|
|
|
|
|
}
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
|
2025-08-19 09:00:31 -07:00
|
|
|
|
/// Render a summary of configured MCP servers from the current `Config`.
|
|
|
|
|
|
pub(crate) fn empty_mcp_output() -> PlainHistoryCell {
|
|
|
|
|
|
let lines: Vec<Line<'static>> = vec![
|
2025-09-02 16:19:54 -07:00
|
|
|
|
"/mcp".magenta().into(),
|
|
|
|
|
|
"".into(),
|
|
|
|
|
|
vec!["🔌 ".into(), "MCP Tools".bold()].into(),
|
|
|
|
|
|
"".into(),
|
|
|
|
|
|
" • No MCP servers configured.".italic().into(),
|
2025-08-20 14:58:04 -07:00
|
|
|
|
Line::from(vec![
|
|
|
|
|
|
" See the ".into(),
|
2025-09-03 00:33:50 -06:00
|
|
|
|
"\u{1b}]8;;https://github.com/openai/codex/blob/main/docs/config.md#mcp_servers\u{7}MCP docs\u{1b}]8;;\u{7}".underlined(),
|
2025-08-20 14:58:04 -07:00
|
|
|
|
" to configure them.".into(),
|
|
|
|
|
|
])
|
|
|
|
|
|
.style(Style::default().add_modifier(Modifier::DIM)),
|
2025-08-19 09:00:31 -07:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
PlainHistoryCell { lines }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Render MCP tools grouped by connection using the fully-qualified tool names.
|
|
|
|
|
|
pub(crate) fn new_mcp_tools_output(
|
|
|
|
|
|
config: &Config,
|
|
|
|
|
|
tools: std::collections::HashMap<String, mcp_types::Tool>,
|
|
|
|
|
|
) -> PlainHistoryCell {
|
|
|
|
|
|
let mut lines: Vec<Line<'static>> = vec![
|
2025-09-02 16:19:54 -07:00
|
|
|
|
"/mcp".magenta().into(),
|
|
|
|
|
|
"".into(),
|
|
|
|
|
|
vec!["🔌 ".into(), "MCP Tools".bold()].into(),
|
|
|
|
|
|
"".into(),
|
2025-08-19 09:00:31 -07:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if tools.is_empty() {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(" • No MCP tools available.".italic().into());
|
|
|
|
|
|
lines.push("".into());
|
2025-08-19 09:00:31 -07:00
|
|
|
|
return PlainHistoryCell { lines };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (server, cfg) in config.mcp_servers.iter() {
|
|
|
|
|
|
let prefix = format!("{server}__");
|
|
|
|
|
|
let mut names: Vec<String> = tools
|
|
|
|
|
|
.keys()
|
|
|
|
|
|
.filter(|k| k.starts_with(&prefix))
|
|
|
|
|
|
.map(|k| k[prefix.len()..].to_string())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
names.sort();
|
|
|
|
|
|
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • Server: ".into(), server.clone().into()].into());
|
2025-08-19 09:00:31 -07:00
|
|
|
|
|
|
|
|
|
|
if !cfg.command.is_empty() {
|
|
|
|
|
|
let cmd_display = format!("{} {}", cfg.command, cfg.args.join(" "));
|
|
|
|
|
|
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • Command: ".into(), cmd_display.into()].into());
|
2025-08-19 09:00:31 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if names.is_empty() {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(" • Tools: (none)".into());
|
2025-08-19 09:00:31 -07:00
|
|
|
|
} else {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(vec![" • Tools: ".into(), names.join(", ").into()].into());
|
2025-08-19 09:00:31 -07:00
|
|
|
|
}
|
|
|
|
|
|
lines.push(Line::from(""));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PlainHistoryCell { lines }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
pub(crate) fn new_error_event(message: String) -> PlainHistoryCell {
|
2025-08-25 22:49:19 -07:00
|
|
|
|
// Use a hair space (U+200A) to create a subtle, near-invisible separation
|
|
|
|
|
|
// before the text. VS16 is intentionally omitted to keep spacing tighter
|
|
|
|
|
|
// in terminals like Ghostty.
|
2025-09-02 10:29:58 -07:00
|
|
|
|
let lines: Vec<Line<'static>> =
|
|
|
|
|
|
vec![vec![padded_emoji("🖐").red().bold(), " ".into(), message.into()].into()];
|
2025-08-14 14:10:05 -04:00
|
|
|
|
PlainHistoryCell { lines }
|
|
|
|
|
|
}
|
2025-08-07 00:01:38 -07:00
|
|
|
|
|
2025-08-21 01:15:24 -07:00
|
|
|
|
pub(crate) fn new_stream_error_event(message: String) -> PlainHistoryCell {
|
2025-09-02 10:29:58 -07:00
|
|
|
|
let lines: Vec<Line<'static>> = vec![vec![padded_emoji("⚠️").into(), message.dim()].into()];
|
2025-08-21 01:15:24 -07:00
|
|
|
|
PlainHistoryCell { lines }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
/// Render a user‑friendly plan update styled like a checkbox todo list.
|
2025-09-02 10:29:58 -07:00
|
|
|
|
pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> PlanUpdateCell {
|
2025-08-14 14:10:05 -04:00
|
|
|
|
let UpdatePlanArgs { explanation, plan } = update;
|
2025-09-02 10:29:58 -07:00
|
|
|
|
PlanUpdateCell { explanation, plan }
|
|
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
|
2025-09-02 10:29:58 -07:00
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
|
pub(crate) struct PlanUpdateCell {
|
|
|
|
|
|
explanation: Option<String>,
|
|
|
|
|
|
plan: Vec<PlanItemArg>,
|
|
|
|
|
|
}
|
2025-07-31 13:45:52 -07:00
|
|
|
|
|
2025-09-02 10:29:58 -07:00
|
|
|
|
impl HistoryCell for PlanUpdateCell {
|
|
|
|
|
|
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
|
|
|
|
|
let render_note = |text: &str| -> Vec<Line<'static>> {
|
|
|
|
|
|
let wrap_width = width.saturating_sub(4).max(1) as usize;
|
|
|
|
|
|
textwrap::wrap(text, wrap_width)
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.map(|s| s.to_string().dim().italic().into())
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let render_step = |status: &StepStatus, text: &str| -> Vec<Line<'static>> {
|
|
|
|
|
|
let (box_str, step_style) = match status {
|
|
|
|
|
|
StepStatus::Completed => ("✔ ", Style::default().crossed_out().dim()),
|
|
|
|
|
|
StepStatus::InProgress => ("□ ", Style::default().cyan().bold()),
|
|
|
|
|
|
StepStatus::Pending => ("□ ", Style::default().dim()),
|
2025-08-14 14:10:05 -04:00
|
|
|
|
};
|
2025-09-02 10:29:58 -07:00
|
|
|
|
let wrap_width = (width as usize)
|
|
|
|
|
|
.saturating_sub(4)
|
|
|
|
|
|
.saturating_sub(box_str.width())
|
|
|
|
|
|
.max(1);
|
|
|
|
|
|
let parts = textwrap::wrap(text, wrap_width);
|
|
|
|
|
|
let step_text = parts
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.map(|s| s.to_string().set_style(step_style).into())
|
|
|
|
|
|
.collect();
|
2025-09-04 16:54:53 -07:00
|
|
|
|
prefix_lines(step_text, box_str.into(), " ".into())
|
2025-09-02 10:29:58 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let mut lines: Vec<Line<'static>> = vec![];
|
|
|
|
|
|
lines.push(vec!["• ".into(), "Updated Plan".bold()].into());
|
|
|
|
|
|
|
|
|
|
|
|
let mut indented_lines = vec![];
|
|
|
|
|
|
let note = self
|
|
|
|
|
|
.explanation
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|s| s.trim())
|
|
|
|
|
|
.filter(|t| !t.is_empty());
|
|
|
|
|
|
if let Some(expl) = note {
|
|
|
|
|
|
indented_lines.extend(render_note(expl));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if self.plan.is_empty() {
|
|
|
|
|
|
indented_lines.push(Line::from("(no steps provided)".dim().italic()));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for PlanItemArg { step, status } in self.plan.iter() {
|
|
|
|
|
|
indented_lines.extend(render_step(status, step));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-04 16:54:53 -07:00
|
|
|
|
lines.extend(prefix_lines(indented_lines, " └ ".into(), " ".into()));
|
2025-09-02 10:29:58 -07:00
|
|
|
|
|
|
|
|
|
|
lines
|
|
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
/// Create a new `PendingPatch` cell that lists the file‑level summary of
|
|
|
|
|
|
/// a proposed patch. The summary lines should already be formatted (e.g.
|
|
|
|
|
|
/// "A path/to/file.rs").
|
|
|
|
|
|
pub(crate) fn new_patch_event(
|
|
|
|
|
|
event_type: PatchEventType,
|
|
|
|
|
|
changes: HashMap<PathBuf, FileChange>,
|
2025-09-02 10:29:58 -07:00
|
|
|
|
cwd: &Path,
|
|
|
|
|
|
) -> PatchHistoryCell {
|
|
|
|
|
|
PatchHistoryCell {
|
|
|
|
|
|
event_type,
|
|
|
|
|
|
changes,
|
|
|
|
|
|
cwd: cwd.to_path_buf(),
|
|
|
|
|
|
}
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(crate) fn new_patch_apply_failure(stderr: String) -> PlainHistoryCell {
|
|
|
|
|
|
let mut lines: Vec<Line<'static>> = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
// Failure title
|
|
|
|
|
|
lines.push(Line::from("✘ Failed to apply patch".magenta().bold()));
|
|
|
|
|
|
|
|
|
|
|
|
if !stderr.trim().is_empty() {
|
|
|
|
|
|
lines.extend(output_lines(
|
|
|
|
|
|
Some(&CommandOutput {
|
|
|
|
|
|
exit_code: 1,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr,
|
2025-08-22 16:32:31 -07:00
|
|
|
|
formatted_output: String::new(),
|
2025-08-14 14:10:05 -04:00
|
|
|
|
}),
|
|
|
|
|
|
true,
|
|
|
|
|
|
true,
|
2025-09-02 10:29:58 -07:00
|
|
|
|
true,
|
2025-08-14 14:10:05 -04:00
|
|
|
|
));
|
2025-08-05 22:44:27 -07:00
|
|
|
|
}
|
2025-08-12 17:37:28 -07:00
|
|
|
|
|
2025-08-14 14:10:05 -04:00
|
|
|
|
PlainHistoryCell { lines }
|
2025-08-06 12:03:45 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-04 16:54:53 -07:00
|
|
|
|
/// Create a new history cell for a proposed command approval.
|
|
|
|
|
|
/// Renders a header and the command preview similar to how proposed patches
|
|
|
|
|
|
/// show a header and summary.
|
|
|
|
|
|
pub(crate) fn new_proposed_command(command: &[String]) -> PlainHistoryCell {
|
|
|
|
|
|
let cmd = strip_bash_lc_and_escape(command);
|
|
|
|
|
|
|
|
|
|
|
|
let mut lines: Vec<Line<'static>> = Vec::new();
|
|
|
|
|
|
lines.push(Line::from(vec!["• ".into(), "Proposed Command".bold()]));
|
|
|
|
|
|
|
2025-09-08 10:48:41 -07:00
|
|
|
|
let highlighted_lines = crate::render::highlight::highlight_bash_to_lines(&cmd);
|
2025-09-04 16:54:53 -07:00
|
|
|
|
let initial_prefix: Span<'static> = " └ ".dim();
|
|
|
|
|
|
let subsequent_prefix: Span<'static> = " ".into();
|
2025-09-08 10:48:41 -07:00
|
|
|
|
lines.extend(prefix_lines(
|
|
|
|
|
|
highlighted_lines,
|
|
|
|
|
|
initial_prefix,
|
|
|
|
|
|
subsequent_prefix,
|
|
|
|
|
|
));
|
2025-09-04 16:54:53 -07:00
|
|
|
|
|
|
|
|
|
|
PlainHistoryCell { lines }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-20 17:09:46 -07:00
|
|
|
|
pub(crate) fn new_reasoning_block(
|
|
|
|
|
|
full_reasoning_buffer: String,
|
|
|
|
|
|
config: &Config,
|
|
|
|
|
|
) -> TranscriptOnlyHistoryCell {
|
|
|
|
|
|
let mut lines: Vec<Line<'static>> = Vec::new();
|
|
|
|
|
|
lines.push(Line::from("thinking".magenta().italic()));
|
|
|
|
|
|
append_markdown(&full_reasoning_buffer, &mut lines, config);
|
|
|
|
|
|
TranscriptOnlyHistoryCell { lines }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-02 18:47:14 -07:00
|
|
|
|
pub(crate) fn new_reasoning_summary_block(
|
|
|
|
|
|
full_reasoning_buffer: String,
|
|
|
|
|
|
config: &Config,
|
|
|
|
|
|
) -> Vec<Box<dyn HistoryCell>> {
|
2025-09-04 11:00:01 -07:00
|
|
|
|
if config.model_family.reasoning_summary_format == ReasoningSummaryFormat::Experimental {
|
2025-09-02 18:47:14 -07:00
|
|
|
|
// Experimental format is following:
|
|
|
|
|
|
// ** header **
|
|
|
|
|
|
//
|
|
|
|
|
|
// reasoning summary
|
|
|
|
|
|
//
|
|
|
|
|
|
// So we need to strip header from reasoning summary
|
2025-09-04 09:45:14 -07:00
|
|
|
|
let full_reasoning_buffer = full_reasoning_buffer.trim();
|
2025-09-02 18:47:14 -07:00
|
|
|
|
if let Some(open) = full_reasoning_buffer.find("**") {
|
|
|
|
|
|
let after_open = &full_reasoning_buffer[(open + 2)..];
|
|
|
|
|
|
if let Some(close) = after_open.find("**") {
|
|
|
|
|
|
let after_close_idx = open + 2 + close + 2;
|
2025-09-04 09:45:14 -07:00
|
|
|
|
// if we don't have anything beyond `after_close_idx`
|
|
|
|
|
|
// then we don't have a summary to inject into history
|
|
|
|
|
|
if after_close_idx < full_reasoning_buffer.len() {
|
|
|
|
|
|
let header_buffer = full_reasoning_buffer[..after_close_idx].to_string();
|
|
|
|
|
|
let summary_buffer = full_reasoning_buffer[after_close_idx..].to_string();
|
|
|
|
|
|
|
|
|
|
|
|
let mut header_lines: Vec<Line<'static>> = Vec::new();
|
|
|
|
|
|
header_lines.push(Line::from("Thinking".magenta().italic()));
|
|
|
|
|
|
append_markdown(&header_buffer, &mut header_lines, config);
|
|
|
|
|
|
|
|
|
|
|
|
let mut summary_lines: Vec<Line<'static>> = Vec::new();
|
|
|
|
|
|
summary_lines.push(Line::from("Thinking".magenta().bold()));
|
|
|
|
|
|
append_markdown(&summary_buffer, &mut summary_lines, config);
|
|
|
|
|
|
|
|
|
|
|
|
return vec![
|
|
|
|
|
|
Box::new(TranscriptOnlyHistoryCell {
|
|
|
|
|
|
lines: header_lines,
|
|
|
|
|
|
}),
|
|
|
|
|
|
Box::new(AgentMessageCell::new(summary_lines, true)),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
2025-09-02 18:47:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
vec![Box::new(new_reasoning_block(full_reasoning_buffer, config))]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-11 11:26:15 -07:00
|
|
|
|
fn output_lines(
|
|
|
|
|
|
output: Option<&CommandOutput>,
|
|
|
|
|
|
only_err: bool,
|
|
|
|
|
|
include_angle_pipe: bool,
|
2025-09-02 10:29:58 -07:00
|
|
|
|
include_prefix: bool,
|
2025-08-11 11:26:15 -07:00
|
|
|
|
) -> Vec<Line<'static>> {
|
|
|
|
|
|
let CommandOutput {
|
|
|
|
|
|
exit_code,
|
|
|
|
|
|
stdout,
|
|
|
|
|
|
stderr,
|
2025-08-22 16:32:31 -07:00
|
|
|
|
..
|
2025-08-11 11:26:15 -07:00
|
|
|
|
} = match output {
|
|
|
|
|
|
Some(output) if only_err && output.exit_code == 0 => return vec![],
|
|
|
|
|
|
Some(output) => output,
|
|
|
|
|
|
None => return vec![],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let src = if *exit_code == 0 { stdout } else { stderr };
|
|
|
|
|
|
let lines: Vec<&str> = src.lines().collect();
|
|
|
|
|
|
let total = lines.len();
|
|
|
|
|
|
let limit = TOOL_CALL_MAX_LINES;
|
|
|
|
|
|
|
|
|
|
|
|
let mut out = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
let head_end = total.min(limit);
|
|
|
|
|
|
for (i, raw) in lines[..head_end].iter().enumerate() {
|
|
|
|
|
|
let mut line = ansi_escape_line(raw);
|
2025-09-02 10:29:58 -07:00
|
|
|
|
let prefix = if !include_prefix {
|
|
|
|
|
|
""
|
|
|
|
|
|
} else if i == 0 && include_angle_pipe {
|
2025-08-13 19:14:03 -04:00
|
|
|
|
" └ "
|
2025-08-11 11:26:15 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
" "
|
|
|
|
|
|
};
|
|
|
|
|
|
line.spans.insert(0, prefix.into());
|
|
|
|
|
|
line.spans.iter_mut().for_each(|span| {
|
|
|
|
|
|
span.style = span.style.add_modifier(Modifier::DIM);
|
|
|
|
|
|
});
|
|
|
|
|
|
out.push(line);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If we will ellipsize less than the limit, just show it.
|
|
|
|
|
|
let show_ellipsis = total > 2 * limit;
|
|
|
|
|
|
if show_ellipsis {
|
|
|
|
|
|
let omitted = total - 2 * limit;
|
2025-09-02 16:19:54 -07:00
|
|
|
|
out.push(format!("… +{omitted} lines").into());
|
2025-08-11 11:26:15 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let tail_start = if show_ellipsis {
|
|
|
|
|
|
total - limit
|
|
|
|
|
|
} else {
|
|
|
|
|
|
head_end
|
|
|
|
|
|
};
|
|
|
|
|
|
for raw in lines[tail_start..].iter() {
|
|
|
|
|
|
let mut line = ansi_escape_line(raw);
|
2025-09-02 10:29:58 -07:00
|
|
|
|
if include_prefix {
|
|
|
|
|
|
line.spans.insert(0, " ".into());
|
|
|
|
|
|
}
|
2025-08-11 11:26:15 -07:00
|
|
|
|
line.spans.iter_mut().for_each(|span| {
|
2025-08-06 22:25:41 -07:00
|
|
|
|
span.style = span.style.add_modifier(Modifier::DIM);
|
|
|
|
|
|
});
|
|
|
|
|
|
out.push(line);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
out
|
feat: initial import of Rust implementation of Codex CLI in codex-rs/ (#629)
As stated in `codex-rs/README.md`:
Today, Codex CLI is written in TypeScript and requires Node.js 22+ to
run it. For a number of users, this runtime requirement inhibits
adoption: they would be better served by a standalone executable. As
maintainers, we want Codex to run efficiently in a wide range of
environments with minimal overhead. We also want to take advantage of
operating system-specific APIs to provide better sandboxing, where
possible.
To that end, we are moving forward with a Rust implementation of Codex
CLI contained in this folder, which has the following benefits:
- The CLI compiles to small, standalone, platform-specific binaries.
- Can make direct, native calls to
[seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and
[landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in
order to support sandboxing on Linux.
- No runtime garbage collection, resulting in lower memory consumption
and better, more predictable performance.
Currently, the Rust implementation is materially behind the TypeScript
implementation in functionality, so continue to use the TypeScript
implmentation for the time being. We will publish native executables via
GitHub Releases as soon as we feel the Rust version is usable.
2025-04-24 13:31:40 -07:00
|
|
|
|
}
|
fix: introduce ResponseInputItem::McpToolCallOutput variant (#1151)
The output of an MCP server tool call can be one of several types, but
to date, we treated all outputs as text by showing the serialized JSON
as the "tool output" in Codex:
https://github.com/openai/codex/blob/25a9949c49194d5a64de54a11bcc5b4724ac9bd5/codex-rs/mcp-types/src/lib.rs#L96-L101
This PR adds support for the `ImageContent` variant so we can now
display an image output from an MCP tool call.
In making this change, we introduce a new
`ResponseInputItem::McpToolCallOutput` variant so that we can work with
the `mcp_types::CallToolResult` directly when the function call is made
to an MCP server.
Though arguably the more significant change is the introduction of
`HistoryCell::CompletedMcpToolCallWithImageOutput`, which is a cell that
uses `ratatui_image` to render an image into the terminal. To support
this, we introduce `ImageRenderCache`, cache a
`ratatui_image::picker::Picker`, and `ensure_image_cache()` to cache the
appropriate scaled image data and dimensions based on the current
terminal size.
To test, I created a minimal `package.json`:
```json
{
"name": "kitty-mcp",
"version": "1.0.0",
"type": "module",
"description": "MCP that returns image of kitty",
"main": "index.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0"
}
}
```
with the following `index.js` to define the MCP server:
```js
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
const IMAGE_URI = "image://Ada.png";
const server = new McpServer({
name: "Demo",
version: "1.0.0",
});
server.tool(
"get-cat-image",
"If you need a cat image, this tool will provide one.",
async () => ({
content: [
{ type: "image", data: await getAdaPngBase64(), mimeType: "image/png" },
],
})
);
server.resource("Ada the Cat", IMAGE_URI, async (uri) => {
const base64Image = await getAdaPngBase64();
return {
contents: [
{
uri: uri.href,
mimeType: "image/png",
blob: base64Image,
},
],
};
});
async function getAdaPngBase64() {
const __dirname = new URL(".", import.meta.url).pathname;
// From https://github.com/benjajaja/ratatui-image/blob/9705ce2c59ec669abbce2924cbfd1f5ae22c9860/assets/Ada.png
const filePath = join(__dirname, "Ada.png");
const imageData = await readFile(filePath);
const base64Image = imageData.toString("base64");
return base64Image;
}
const transport = new StdioServerTransport();
await server.connect(transport);
```
With the local changes from this PR, I added the following to my
`config.toml`:
```toml
[mcp_servers.kitty]
command = "node"
args = ["/Users/mbolin/code/kitty-mcp/index.js"]
```
Running the TUI from source:
```
cargo run --bin codex -- --model o3 'I need a picture of a cat'
```
I get:
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/bf80b721-9ca0-4d81-aec7-77d6899e2869"
/>
Now, that said, I have only tested in iTerm and there is definitely some
funny business with getting an accurate character-to-pixel ratio
(sometimes the `CompletedMcpToolCallWithImageOutput` thinks it needs 10
rows to render instead of 4), so there is still work to be done here.
2025-05-28 19:03:17 -07:00
|
|
|
|
|
2025-07-30 10:05:40 -07:00
|
|
|
|
fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
|
|
|
|
|
|
let args_str = invocation
|
|
|
|
|
|
.arguments
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|v| {
|
|
|
|
|
|
// Use compact form to keep things short but readable.
|
|
|
|
|
|
serde_json::to_string(v).unwrap_or_else(|_| v.to_string())
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
|
|
let invocation_spans = vec![
|
2025-09-02 16:19:54 -07:00
|
|
|
|
invocation.server.clone().cyan(),
|
|
|
|
|
|
".".into(),
|
2025-09-11 11:59:37 -07:00
|
|
|
|
invocation.tool.cyan(),
|
2025-09-02 16:19:54 -07:00
|
|
|
|
"(".into(),
|
|
|
|
|
|
args_str.dim(),
|
|
|
|
|
|
")".into(),
|
2025-07-30 10:05:40 -07:00
|
|
|
|
];
|
2025-09-02 16:19:54 -07:00
|
|
|
|
invocation_spans.into()
|
fix: introduce ResponseInputItem::McpToolCallOutput variant (#1151)
The output of an MCP server tool call can be one of several types, but
to date, we treated all outputs as text by showing the serialized JSON
as the "tool output" in Codex:
https://github.com/openai/codex/blob/25a9949c49194d5a64de54a11bcc5b4724ac9bd5/codex-rs/mcp-types/src/lib.rs#L96-L101
This PR adds support for the `ImageContent` variant so we can now
display an image output from an MCP tool call.
In making this change, we introduce a new
`ResponseInputItem::McpToolCallOutput` variant so that we can work with
the `mcp_types::CallToolResult` directly when the function call is made
to an MCP server.
Though arguably the more significant change is the introduction of
`HistoryCell::CompletedMcpToolCallWithImageOutput`, which is a cell that
uses `ratatui_image` to render an image into the terminal. To support
this, we introduce `ImageRenderCache`, cache a
`ratatui_image::picker::Picker`, and `ensure_image_cache()` to cache the
appropriate scaled image data and dimensions based on the current
terminal size.
To test, I created a minimal `package.json`:
```json
{
"name": "kitty-mcp",
"version": "1.0.0",
"type": "module",
"description": "MCP that returns image of kitty",
"main": "index.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0"
}
}
```
with the following `index.js` to define the MCP server:
```js
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
const IMAGE_URI = "image://Ada.png";
const server = new McpServer({
name: "Demo",
version: "1.0.0",
});
server.tool(
"get-cat-image",
"If you need a cat image, this tool will provide one.",
async () => ({
content: [
{ type: "image", data: await getAdaPngBase64(), mimeType: "image/png" },
],
})
);
server.resource("Ada the Cat", IMAGE_URI, async (uri) => {
const base64Image = await getAdaPngBase64();
return {
contents: [
{
uri: uri.href,
mimeType: "image/png",
blob: base64Image,
},
],
};
});
async function getAdaPngBase64() {
const __dirname = new URL(".", import.meta.url).pathname;
// From https://github.com/benjajaja/ratatui-image/blob/9705ce2c59ec669abbce2924cbfd1f5ae22c9860/assets/Ada.png
const filePath = join(__dirname, "Ada.png");
const imageData = await readFile(filePath);
const base64Image = imageData.toString("base64");
return base64Image;
}
const transport = new StdioServerTransport();
await server.connect(transport);
```
With the local changes from this PR, I added the following to my
`config.toml`:
```toml
[mcp_servers.kitty]
command = "node"
args = ["/Users/mbolin/code/kitty-mcp/index.js"]
```
Running the TUI from source:
```
cargo run --bin codex -- --model o3 'I need a picture of a cat'
```
I get:
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/bf80b721-9ca0-4d81-aec7-77d6899e2869"
/>
Now, that said, I have only tested in iTerm and there is definitely some
funny business with getting an accurate character-to-pixel ratio
(sometimes the `CompletedMcpToolCallWithImageOutput` thinks it needs 10
rows to render instead of 4), so there is still work to be done here.
2025-05-28 19:03:17 -07:00
|
|
|
|
}
|
2025-08-11 11:26:15 -07:00
|
|
|
|
|
2025-08-11 16:11:46 -07:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
|
|
|
|
|
use super::*;
|
2025-09-04 09:45:14 -07:00
|
|
|
|
use codex_core::config::Config;
|
|
|
|
|
|
use codex_core::config::ConfigOverrides;
|
|
|
|
|
|
use codex_core::config::ConfigToml;
|
|
|
|
|
|
|
|
|
|
|
|
fn test_config() -> Config {
|
|
|
|
|
|
Config::load_from_base_config_with_overrides(
|
|
|
|
|
|
ConfigToml::default(),
|
|
|
|
|
|
ConfigOverrides::default(),
|
|
|
|
|
|
std::env::temp_dir(),
|
|
|
|
|
|
)
|
|
|
|
|
|
.expect("config")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn render_lines(lines: &[Line<'static>]) -> Vec<String> {
|
|
|
|
|
|
lines
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|line| {
|
|
|
|
|
|
line.spans
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|span| span.content.as_ref())
|
|
|
|
|
|
.collect::<String>()
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn render_transcript(cell: &dyn HistoryCell) -> Vec<String> {
|
|
|
|
|
|
render_lines(&cell.transcript_lines())
|
|
|
|
|
|
}
|
2025-08-11 16:11:46 -07:00
|
|
|
|
|
|
|
|
|
|
#[test]
|
2025-09-02 10:29:58 -07:00
|
|
|
|
fn coalesces_sequential_reads_within_one_call() {
|
|
|
|
|
|
// Build one exec cell with a Search followed by two Reads
|
|
|
|
|
|
let call_id = "c1".to_string();
|
|
|
|
|
|
let mut cell = ExecCell::new(ExecCall {
|
|
|
|
|
|
call_id: call_id.clone(),
|
|
|
|
|
|
command: vec!["bash".into(), "-lc".into(), "echo".into()],
|
|
|
|
|
|
parsed: vec![
|
|
|
|
|
|
ParsedCommand::Search {
|
|
|
|
|
|
query: Some("shimmer_spans".into()),
|
|
|
|
|
|
path: None,
|
|
|
|
|
|
cmd: "rg shimmer_spans".into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
ParsedCommand::Read {
|
|
|
|
|
|
name: "shimmer.rs".into(),
|
|
|
|
|
|
cmd: "cat shimmer.rs".into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
ParsedCommand::Read {
|
|
|
|
|
|
name: "status_indicator_widget.rs".into(),
|
|
|
|
|
|
cmd: "cat status_indicator_widget.rs".into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
output: None,
|
|
|
|
|
|
start_time: Some(Instant::now()),
|
|
|
|
|
|
duration: None,
|
|
|
|
|
|
});
|
|
|
|
|
|
// Mark call complete so markers are ✓
|
|
|
|
|
|
cell.complete_call(
|
|
|
|
|
|
&call_id,
|
|
|
|
|
|
CommandOutput {
|
|
|
|
|
|
exit_code: 0,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr: String::new(),
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Duration::from_millis(1),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let lines = cell.display_lines(80);
|
2025-09-04 09:45:14 -07:00
|
|
|
|
let rendered = render_lines(&lines).join("\n");
|
2025-09-02 10:29:58 -07:00
|
|
|
|
insta::assert_snapshot!(rendered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn coalesces_reads_across_multiple_calls() {
|
|
|
|
|
|
let mut cell = ExecCell::new(ExecCall {
|
|
|
|
|
|
call_id: "c1".to_string(),
|
|
|
|
|
|
command: vec!["bash".into(), "-lc".into(), "echo".into()],
|
|
|
|
|
|
parsed: vec![ParsedCommand::Search {
|
|
|
|
|
|
query: Some("shimmer_spans".into()),
|
|
|
|
|
|
path: None,
|
|
|
|
|
|
cmd: "rg shimmer_spans".into(),
|
|
|
|
|
|
}],
|
|
|
|
|
|
output: None,
|
|
|
|
|
|
start_time: Some(Instant::now()),
|
|
|
|
|
|
duration: None,
|
|
|
|
|
|
});
|
|
|
|
|
|
// Call 1: Search only
|
|
|
|
|
|
cell.complete_call(
|
|
|
|
|
|
"c1",
|
|
|
|
|
|
CommandOutput {
|
|
|
|
|
|
exit_code: 0,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr: String::new(),
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Duration::from_millis(1),
|
|
|
|
|
|
);
|
|
|
|
|
|
// Call 2: Read A
|
|
|
|
|
|
cell = cell
|
|
|
|
|
|
.with_added_call(
|
|
|
|
|
|
"c2".into(),
|
|
|
|
|
|
vec!["bash".into(), "-lc".into(), "echo".into()],
|
|
|
|
|
|
vec![ParsedCommand::Read {
|
|
|
|
|
|
name: "shimmer.rs".into(),
|
|
|
|
|
|
cmd: "cat shimmer.rs".into(),
|
|
|
|
|
|
}],
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
cell.complete_call(
|
|
|
|
|
|
"c2",
|
|
|
|
|
|
CommandOutput {
|
|
|
|
|
|
exit_code: 0,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr: String::new(),
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Duration::from_millis(1),
|
|
|
|
|
|
);
|
|
|
|
|
|
// Call 3: Read B
|
|
|
|
|
|
cell = cell
|
|
|
|
|
|
.with_added_call(
|
|
|
|
|
|
"c3".into(),
|
|
|
|
|
|
vec!["bash".into(), "-lc".into(), "echo".into()],
|
|
|
|
|
|
vec![ParsedCommand::Read {
|
|
|
|
|
|
name: "status_indicator_widget.rs".into(),
|
|
|
|
|
|
cmd: "cat status_indicator_widget.rs".into(),
|
|
|
|
|
|
}],
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
cell.complete_call(
|
|
|
|
|
|
"c3",
|
|
|
|
|
|
CommandOutput {
|
|
|
|
|
|
exit_code: 0,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr: String::new(),
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Duration::from_millis(1),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let lines = cell.display_lines(80);
|
2025-09-04 09:45:14 -07:00
|
|
|
|
let rendered = render_lines(&lines).join("\n");
|
2025-09-02 10:29:58 -07:00
|
|
|
|
insta::assert_snapshot!(rendered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn coalesced_reads_dedupe_names() {
|
|
|
|
|
|
let mut cell = ExecCell::new(ExecCall {
|
|
|
|
|
|
call_id: "c1".to_string(),
|
|
|
|
|
|
command: vec!["bash".into(), "-lc".into(), "echo".into()],
|
|
|
|
|
|
parsed: vec![
|
|
|
|
|
|
ParsedCommand::Read {
|
|
|
|
|
|
name: "auth.rs".into(),
|
|
|
|
|
|
cmd: "cat auth.rs".into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
ParsedCommand::Read {
|
|
|
|
|
|
name: "auth.rs".into(),
|
|
|
|
|
|
cmd: "cat auth.rs".into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
ParsedCommand::Read {
|
|
|
|
|
|
name: "shimmer.rs".into(),
|
|
|
|
|
|
cmd: "cat shimmer.rs".into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
output: None,
|
|
|
|
|
|
start_time: Some(Instant::now()),
|
|
|
|
|
|
duration: None,
|
|
|
|
|
|
});
|
|
|
|
|
|
cell.complete_call(
|
|
|
|
|
|
"c1",
|
|
|
|
|
|
CommandOutput {
|
|
|
|
|
|
exit_code: 0,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr: String::new(),
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Duration::from_millis(1),
|
|
|
|
|
|
);
|
|
|
|
|
|
let lines = cell.display_lines(80);
|
2025-09-04 09:45:14 -07:00
|
|
|
|
let rendered = render_lines(&lines).join("\n");
|
2025-09-02 10:29:58 -07:00
|
|
|
|
insta::assert_snapshot!(rendered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn multiline_command_wraps_with_extra_indent_on_subsequent_lines() {
|
|
|
|
|
|
// Create a completed exec cell with a multiline command
|
|
|
|
|
|
let cmd = "set -o pipefail\ncargo test --all-features --quiet".to_string();
|
|
|
|
|
|
let call_id = "c1".to_string();
|
|
|
|
|
|
let mut cell = ExecCell::new(ExecCall {
|
|
|
|
|
|
call_id: call_id.clone(),
|
|
|
|
|
|
command: vec!["bash".into(), "-lc".into(), cmd],
|
|
|
|
|
|
parsed: Vec::new(),
|
|
|
|
|
|
output: None,
|
|
|
|
|
|
start_time: Some(Instant::now()),
|
|
|
|
|
|
duration: None,
|
|
|
|
|
|
});
|
|
|
|
|
|
// Mark call complete so it renders as "Ran"
|
|
|
|
|
|
cell.complete_call(
|
|
|
|
|
|
&call_id,
|
|
|
|
|
|
CommandOutput {
|
|
|
|
|
|
exit_code: 0,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr: String::new(),
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Duration::from_millis(1),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Small width to force wrapping on both lines
|
|
|
|
|
|
let width: u16 = 28;
|
|
|
|
|
|
let lines = cell.display_lines(width);
|
2025-09-04 09:45:14 -07:00
|
|
|
|
let rendered = render_lines(&lines).join("\n");
|
2025-09-02 10:29:58 -07:00
|
|
|
|
insta::assert_snapshot!(rendered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn single_line_command_compact_when_fits() {
|
|
|
|
|
|
let call_id = "c1".to_string();
|
|
|
|
|
|
let mut cell = ExecCell::new(ExecCall {
|
|
|
|
|
|
call_id: call_id.clone(),
|
|
|
|
|
|
command: vec!["echo".into(), "ok".into()],
|
|
|
|
|
|
parsed: Vec::new(),
|
|
|
|
|
|
output: None,
|
|
|
|
|
|
start_time: Some(Instant::now()),
|
|
|
|
|
|
duration: None,
|
|
|
|
|
|
});
|
|
|
|
|
|
cell.complete_call(
|
|
|
|
|
|
&call_id,
|
|
|
|
|
|
CommandOutput {
|
|
|
|
|
|
exit_code: 0,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr: String::new(),
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Duration::from_millis(1),
|
|
|
|
|
|
);
|
|
|
|
|
|
// Wide enough that it fits inline
|
|
|
|
|
|
let lines = cell.display_lines(80);
|
2025-09-04 09:45:14 -07:00
|
|
|
|
let rendered = render_lines(&lines).join("\n");
|
2025-09-02 10:29:58 -07:00
|
|
|
|
insta::assert_snapshot!(rendered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn single_line_command_wraps_with_four_space_continuation() {
|
|
|
|
|
|
let call_id = "c1".to_string();
|
|
|
|
|
|
let long = "a_very_long_token_without_spaces_to_force_wrapping".to_string();
|
|
|
|
|
|
let mut cell = ExecCell::new(ExecCall {
|
|
|
|
|
|
call_id: call_id.clone(),
|
|
|
|
|
|
command: vec!["bash".into(), "-lc".into(), long],
|
|
|
|
|
|
parsed: Vec::new(),
|
|
|
|
|
|
output: None,
|
|
|
|
|
|
start_time: Some(Instant::now()),
|
|
|
|
|
|
duration: None,
|
|
|
|
|
|
});
|
|
|
|
|
|
cell.complete_call(
|
|
|
|
|
|
&call_id,
|
|
|
|
|
|
CommandOutput {
|
|
|
|
|
|
exit_code: 0,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr: String::new(),
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Duration::from_millis(1),
|
|
|
|
|
|
);
|
|
|
|
|
|
let lines = cell.display_lines(24);
|
2025-09-04 09:45:14 -07:00
|
|
|
|
let rendered = render_lines(&lines).join("\n");
|
2025-09-02 10:29:58 -07:00
|
|
|
|
insta::assert_snapshot!(rendered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn multiline_command_without_wrap_uses_branch_then_eight_spaces() {
|
|
|
|
|
|
let call_id = "c1".to_string();
|
|
|
|
|
|
let cmd = "echo one\necho two".to_string();
|
|
|
|
|
|
let mut cell = ExecCell::new(ExecCall {
|
|
|
|
|
|
call_id: call_id.clone(),
|
|
|
|
|
|
command: vec!["bash".into(), "-lc".into(), cmd],
|
|
|
|
|
|
parsed: Vec::new(),
|
|
|
|
|
|
output: None,
|
|
|
|
|
|
start_time: Some(Instant::now()),
|
|
|
|
|
|
duration: None,
|
|
|
|
|
|
});
|
|
|
|
|
|
cell.complete_call(
|
|
|
|
|
|
&call_id,
|
|
|
|
|
|
CommandOutput {
|
|
|
|
|
|
exit_code: 0,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr: String::new(),
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Duration::from_millis(1),
|
|
|
|
|
|
);
|
|
|
|
|
|
let lines = cell.display_lines(80);
|
2025-09-04 09:45:14 -07:00
|
|
|
|
let rendered = render_lines(&lines).join("\n");
|
2025-09-02 10:29:58 -07:00
|
|
|
|
insta::assert_snapshot!(rendered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn multiline_command_both_lines_wrap_with_correct_prefixes() {
|
|
|
|
|
|
let call_id = "c1".to_string();
|
|
|
|
|
|
let cmd = "first_token_is_long_enough_to_wrap\nsecond_token_is_also_long_enough_to_wrap"
|
|
|
|
|
|
.to_string();
|
|
|
|
|
|
let mut cell = ExecCell::new(ExecCall {
|
|
|
|
|
|
call_id: call_id.clone(),
|
|
|
|
|
|
command: vec!["bash".into(), "-lc".into(), cmd],
|
|
|
|
|
|
parsed: Vec::new(),
|
|
|
|
|
|
output: None,
|
|
|
|
|
|
start_time: Some(Instant::now()),
|
|
|
|
|
|
duration: None,
|
|
|
|
|
|
});
|
|
|
|
|
|
cell.complete_call(
|
|
|
|
|
|
&call_id,
|
|
|
|
|
|
CommandOutput {
|
|
|
|
|
|
exit_code: 0,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr: String::new(),
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Duration::from_millis(1),
|
|
|
|
|
|
);
|
|
|
|
|
|
let lines = cell.display_lines(28);
|
2025-09-04 09:45:14 -07:00
|
|
|
|
let rendered = render_lines(&lines).join("\n");
|
2025-09-02 10:29:58 -07:00
|
|
|
|
insta::assert_snapshot!(rendered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn stderr_tail_more_than_five_lines_snapshot() {
|
|
|
|
|
|
// Build an exec cell with a non-zero exit and 10 lines on stderr to exercise
|
|
|
|
|
|
// the head/tail rendering and gutter prefixes.
|
|
|
|
|
|
let call_id = "c_err".to_string();
|
|
|
|
|
|
let mut cell = ExecCell::new(ExecCall {
|
|
|
|
|
|
call_id: call_id.clone(),
|
|
|
|
|
|
command: vec!["bash".into(), "-lc".into(), "seq 1 10 1>&2 && false".into()],
|
|
|
|
|
|
parsed: Vec::new(),
|
|
|
|
|
|
output: None,
|
|
|
|
|
|
start_time: Some(Instant::now()),
|
|
|
|
|
|
duration: None,
|
|
|
|
|
|
});
|
|
|
|
|
|
let stderr: String = (1..=10)
|
|
|
|
|
|
.map(|n| n.to_string())
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.join("\n");
|
|
|
|
|
|
cell.complete_call(
|
|
|
|
|
|
&call_id,
|
|
|
|
|
|
CommandOutput {
|
|
|
|
|
|
exit_code: 1,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr,
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Duration::from_millis(1),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let rendered = cell
|
|
|
|
|
|
.display_lines(80)
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|l| {
|
|
|
|
|
|
l.spans
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|s| s.content.as_ref())
|
|
|
|
|
|
.collect::<String>()
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.join("\n");
|
|
|
|
|
|
insta::assert_snapshot!(rendered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn ran_cell_multiline_with_stderr_snapshot() {
|
|
|
|
|
|
// Build an exec cell that completes (so it renders as "Ran") with a
|
|
|
|
|
|
// command long enough that it must render on its own line under the
|
|
|
|
|
|
// header, and include a couple of stderr lines to verify the output
|
|
|
|
|
|
// block prefixes and wrapping.
|
|
|
|
|
|
let call_id = "c_wrap_err".to_string();
|
|
|
|
|
|
let long_cmd =
|
|
|
|
|
|
"echo this_is_a_very_long_single_token_that_will_wrap_across_the_available_width";
|
|
|
|
|
|
let mut cell = ExecCell::new(ExecCall {
|
|
|
|
|
|
call_id: call_id.clone(),
|
|
|
|
|
|
command: vec!["bash".into(), "-lc".into(), long_cmd.to_string()],
|
|
|
|
|
|
parsed: Vec::new(),
|
|
|
|
|
|
output: None,
|
|
|
|
|
|
start_time: Some(Instant::now()),
|
|
|
|
|
|
duration: None,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let stderr = "error: first line on stderr\nerror: second line on stderr".to_string();
|
|
|
|
|
|
cell.complete_call(
|
|
|
|
|
|
&call_id,
|
|
|
|
|
|
CommandOutput {
|
|
|
|
|
|
exit_code: 1,
|
|
|
|
|
|
stdout: String::new(),
|
|
|
|
|
|
stderr,
|
|
|
|
|
|
formatted_output: String::new(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Duration::from_millis(5),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Narrow width to force the command to render under the header line.
|
|
|
|
|
|
let width: u16 = 28;
|
|
|
|
|
|
let rendered = cell
|
|
|
|
|
|
.display_lines(width)
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|l| {
|
|
|
|
|
|
l.spans
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|s| s.content.as_ref())
|
|
|
|
|
|
.collect::<String>()
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.join("\n");
|
|
|
|
|
|
insta::assert_snapshot!(rendered);
|
|
|
|
|
|
}
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn user_history_cell_wraps_and_prefixes_each_line_snapshot() {
|
|
|
|
|
|
let msg = "one two three four five six seven";
|
|
|
|
|
|
let cell = UserHistoryCell {
|
|
|
|
|
|
message: msg.to_string(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Small width to force wrapping more clearly. Effective wrap width is width-1 due to the ▌ prefix.
|
|
|
|
|
|
let width: u16 = 12;
|
|
|
|
|
|
let lines = cell.display_lines(width);
|
2025-09-04 09:45:14 -07:00
|
|
|
|
let rendered = render_lines(&lines).join("\n");
|
2025-09-02 10:29:58 -07:00
|
|
|
|
|
|
|
|
|
|
insta::assert_snapshot!(rendered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn plan_update_with_note_and_wrapping_snapshot() {
|
|
|
|
|
|
// Long explanation forces wrapping; include long step text to verify step wrapping and alignment.
|
|
|
|
|
|
let update = UpdatePlanArgs {
|
|
|
|
|
|
explanation: Some(
|
|
|
|
|
|
"I’ll update Grafana call error handling by adding retries and clearer messages when the backend is unreachable."
|
|
|
|
|
|
.to_string(),
|
|
|
|
|
|
),
|
|
|
|
|
|
plan: vec![
|
|
|
|
|
|
PlanItemArg {
|
|
|
|
|
|
step: "Investigate existing error paths and logging around HTTP timeouts".into(),
|
|
|
|
|
|
status: StepStatus::Completed,
|
|
|
|
|
|
},
|
|
|
|
|
|
PlanItemArg {
|
|
|
|
|
|
step: "Harden Grafana client error handling with retry/backoff and user‑friendly messages".into(),
|
|
|
|
|
|
status: StepStatus::InProgress,
|
|
|
|
|
|
},
|
|
|
|
|
|
PlanItemArg {
|
|
|
|
|
|
step: "Add tests for transient failure scenarios and surfacing to the UI".into(),
|
|
|
|
|
|
status: StepStatus::Pending,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let cell = new_plan_update(update);
|
|
|
|
|
|
// Narrow width to force wrapping for both the note and steps
|
|
|
|
|
|
let lines = cell.display_lines(32);
|
2025-09-04 09:45:14 -07:00
|
|
|
|
let rendered = render_lines(&lines).join("\n");
|
2025-09-02 10:29:58 -07:00
|
|
|
|
insta::assert_snapshot!(rendered);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn plan_update_without_note_snapshot() {
|
|
|
|
|
|
let update = UpdatePlanArgs {
|
|
|
|
|
|
explanation: None,
|
|
|
|
|
|
plan: vec![
|
|
|
|
|
|
PlanItemArg {
|
|
|
|
|
|
step: "Define error taxonomy".into(),
|
|
|
|
|
|
status: StepStatus::InProgress,
|
|
|
|
|
|
},
|
|
|
|
|
|
PlanItemArg {
|
|
|
|
|
|
step: "Implement mapping to user messages".into(),
|
|
|
|
|
|
status: StepStatus::Pending,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let cell = new_plan_update(update);
|
|
|
|
|
|
let lines = cell.display_lines(40);
|
2025-09-04 09:45:14 -07:00
|
|
|
|
let rendered = render_lines(&lines).join("\n");
|
2025-09-02 10:29:58 -07:00
|
|
|
|
insta::assert_snapshot!(rendered);
|
2025-08-11 16:11:46 -07:00
|
|
|
|
}
|
2025-09-04 09:45:14 -07:00
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn reasoning_summary_block_returns_reasoning_cell_when_feature_disabled() {
|
|
|
|
|
|
let mut config = test_config();
|
2025-09-04 11:00:01 -07:00
|
|
|
|
config.model_family.reasoning_summary_format = ReasoningSummaryFormat::Experimental;
|
2025-09-04 09:45:14 -07:00
|
|
|
|
|
|
|
|
|
|
let cells =
|
|
|
|
|
|
new_reasoning_summary_block("Detailed reasoning goes here.".to_string(), &config);
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(cells.len(), 1);
|
|
|
|
|
|
let rendered = render_transcript(cells[0].as_ref());
|
|
|
|
|
|
assert_eq!(rendered, vec!["thinking", "Detailed reasoning goes here."]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn reasoning_summary_block_falls_back_when_header_is_missing() {
|
|
|
|
|
|
let mut config = test_config();
|
2025-09-04 11:00:01 -07:00
|
|
|
|
config.model_family.reasoning_summary_format = ReasoningSummaryFormat::Experimental;
|
2025-09-04 09:45:14 -07:00
|
|
|
|
|
|
|
|
|
|
let cells = new_reasoning_summary_block(
|
|
|
|
|
|
"**High level reasoning without closing".to_string(),
|
|
|
|
|
|
&config,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(cells.len(), 1);
|
|
|
|
|
|
let rendered = render_transcript(cells[0].as_ref());
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
rendered,
|
|
|
|
|
|
vec!["thinking", "**High level reasoning without closing"]
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn reasoning_summary_block_falls_back_when_summary_is_missing() {
|
|
|
|
|
|
let mut config = test_config();
|
2025-09-04 11:00:01 -07:00
|
|
|
|
config.model_family.reasoning_summary_format = ReasoningSummaryFormat::Experimental;
|
2025-09-04 09:45:14 -07:00
|
|
|
|
|
|
|
|
|
|
let cells = new_reasoning_summary_block(
|
|
|
|
|
|
"**High level reasoning without closing**".to_string(),
|
|
|
|
|
|
&config,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(cells.len(), 1);
|
|
|
|
|
|
let rendered = render_transcript(cells[0].as_ref());
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
rendered,
|
|
|
|
|
|
vec!["thinking", "High level reasoning without closing"]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let cells = new_reasoning_summary_block(
|
|
|
|
|
|
"**High level reasoning without closing**\n\n ".to_string(),
|
|
|
|
|
|
&config,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(cells.len(), 1);
|
|
|
|
|
|
let rendered = render_transcript(cells[0].as_ref());
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
rendered,
|
|
|
|
|
|
vec!["thinking", "High level reasoning without closing"]
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn reasoning_summary_block_splits_header_and_summary_when_present() {
|
|
|
|
|
|
let mut config = test_config();
|
2025-09-04 11:00:01 -07:00
|
|
|
|
config.model_family.reasoning_summary_format = ReasoningSummaryFormat::Experimental;
|
2025-09-04 09:45:14 -07:00
|
|
|
|
|
|
|
|
|
|
let cells = new_reasoning_summary_block(
|
|
|
|
|
|
"**High level plan**\n\nWe should fix the bug next.".to_string(),
|
|
|
|
|
|
&config,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(cells.len(), 2);
|
|
|
|
|
|
|
|
|
|
|
|
let header_lines = render_transcript(cells[0].as_ref());
|
|
|
|
|
|
assert_eq!(header_lines, vec!["Thinking", "High level plan"]);
|
|
|
|
|
|
|
|
|
|
|
|
let summary_lines = render_transcript(cells[1].as_ref());
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
summary_lines,
|
|
|
|
|
|
vec!["codex", "Thinking", "We should fix the bug next."]
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-08-11 16:11:46 -07:00
|
|
|
|
}
|