Skip to content

Commit a35e679

Browse files
authored
Terminal UI redesign, modeled after codex CLI. (#91)
1 parent d64d5d5 commit a35e679

36 files changed

Lines changed: 8366 additions & 1681 deletions

Cargo.lock

Lines changed: 524 additions & 343 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
members = ["crates/code_assistant", "crates/command_executor", "crates/fs_explorer", "crates/llm", "crates/sandbox", "crates/web"]
33

44
resolver = "2"
5+
6+
[patch.crates-io]
7+
crossterm = { git = "https://github.com/nornagon/crossterm", branch = "nornagon/color-query" }
8+
ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" }

crates/code_assistant/Cargo.toml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,18 @@ tempfile = "3.23"
2020

2121
# Terminal UI
2222
rustyline = "12.0.0"
23-
crossterm = "0.27.0"
23+
crossterm = { version = "0.28.1", features = ["bracketed-paste", "event-stream"] }
2424
unicode-width = "0.1"
25-
ratatui = "0.29"
26-
tui-textarea = "0.7"
25+
ratatui = { version = "0.29", features = [
26+
"scrolling-regions",
27+
"unstable-backend-writer",
28+
"unstable-rendered-line-info",
29+
"unstable-widget-ref",
30+
] }
31+
textwrap = "0.16"
2732
# Pin to 0.3.6 to avoid incompatibility with ratatui 0.29 (0.3.7 requires ratatui-core)
2833
tui-markdown = "=0.3.6"
34+
derive_more = { version = "2", features = ["is_variant"] }
2935

3036
# GPUI related
3137
gpui = "0.2.2"
@@ -64,13 +70,17 @@ rand = "0.8.5"
6470
# Diff visualization
6571
similar = { version = "2.7.0", features = ["inline"] }
6672
async-channel = "2.5.0"
73+
indexmap = "2"
6774

6875
# Base64 encoding for images
6976
base64 = "0.22"
7077

7178
# Image processing
7279
image = "0.25"
7380

81+
# Clipboard access for paste
82+
arboard = "3"
83+
7484
# Agent Client Protocol
7585
agent-client-protocol = { version = "0.9", features = ["unstable"] }
7686
tokio-util = { version = "0.7", features = ["compat"] }

crates/code_assistant/src/acp/ui.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,9 @@ impl UserInterface for ACPUserUI {
568568
for attachment in attachments {
569569
#[allow(clippy::single_match)]
570570
match attachment {
571-
crate::persistence::DraftAttachment::Image { content, mime_type } => {
571+
crate::persistence::DraftAttachment::Image {
572+
content, mime_type, ..
573+
} => {
572574
self.send_session_update(acp::SessionUpdate::UserMessageChunk(
573575
Self::content_chunk(acp::ContentBlock::Image(
574576
acp::ImageContent::new(content, mime_type),

crates/code_assistant/src/persistence.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,15 @@ pub enum DraftAttachment {
892892
#[serde(rename = "text")]
893893
Text { content: String },
894894
#[serde(rename = "image")]
895-
Image { content: String, mime_type: String }, // Base64 encoded
895+
Image {
896+
content: String,
897+
mime_type: String,
898+
/// Image dimensions (optional, for display purposes)
899+
#[serde(default, skip_serializing_if = "Option::is_none")]
900+
width: Option<u32>,
901+
#[serde(default, skip_serializing_if = "Option::is_none")]
902+
height: Option<u32>,
903+
},
896904
#[serde(rename = "file")]
897905
File {
898906
content: String,

crates/code_assistant/src/tools/tool_use_filter.rs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
//! Tool use filtering system to control which tool blocks are allowed and when to truncate responses
22
3+
/// Check if a tool is a read/explore operation (doesn't modify state).
4+
/// Used by the UI to render these tools in compact mode.
5+
pub fn is_explore_tool(tool_name: &str) -> bool {
6+
matches!(
7+
tool_name,
8+
"read_files"
9+
| "list_files"
10+
| "list_projects"
11+
| "search_files"
12+
| "glob_files"
13+
| "web_fetch"
14+
| "web_search"
15+
)
16+
}
17+
18+
/// Check if a tool is a write/edit operation that should show diffs.
19+
#[allow(dead_code)]
20+
pub fn is_write_tool(tool_name: &str) -> bool {
21+
matches!(tool_name, "edit" | "write_file" | "replace_in_file")
22+
}
23+
324
/// Trait for filtering tool use blocks during parsing
425
/// This allows controlling which tools can be used and when to stop parsing
526
pub trait ToolUseFilter: Send + Sync {
@@ -54,20 +75,11 @@ impl SmartToolFilter {
5475
Self {}
5576
}
5677

57-
/// Check if a tool is a "read" operation (doesn't modify state)
78+
/// Check if a tool is a "read" operation (doesn't modify state).
79+
/// Includes explore tools plus utility tools (name_session, update_plan)
80+
/// that are safe to chain.
5881
fn is_read_tool(&self, tool_name: &str) -> bool {
59-
matches!(
60-
tool_name,
61-
"read_files"
62-
| "name_session"
63-
| "update_plan"
64-
| "list_files"
65-
| "list_projects"
66-
| "search_files"
67-
| "glob_files"
68-
| "web_fetch"
69-
| "web_search"
70-
)
82+
is_explore_tool(tool_name) || matches!(tool_name, "name_session" | "update_plan")
7183
}
7284
}
7385

crates/code_assistant/src/ui/backend.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,8 @@ async fn handle_start_message_edit(
752752
} => Some(DraftAttachment::Image {
753753
content: data.clone(),
754754
mime_type: media_type.clone(),
755+
width: None,
756+
height: None,
755757
}),
756758
_ => None,
757759
})

crates/code_assistant/src/ui/gpui/attachment.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ impl AttachmentView {
3737

3838
fn render_content(&self, cx: &mut Context<Self>) -> gpui::AnyElement {
3939
match &self.attachment {
40-
DraftAttachment::Image { mime_type, content } => {
40+
DraftAttachment::Image {
41+
mime_type, content, ..
42+
} => {
4143
// Try to parse and render the actual image
4244
let parsed_image = image::parse_base64_image(mime_type, content);
4345

crates/code_assistant/src/ui/gpui/input_area.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ impl InputArea {
239239
let attachment = DraftAttachment::Image {
240240
content: base64::engine::general_purpose::STANDARD.encode(&image.bytes),
241241
mime_type: image.format.mime_type().to_string(),
242+
width: None,
243+
height: None,
242244
};
243245

244246
self.attachments.push(attachment);

crates/code_assistant/src/ui/gpui/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,11 @@ impl Gpui {
503503
// Add attachments
504504
for attachment in attachments {
505505
match attachment {
506-
crate::persistence::DraftAttachment::Image { content, mime_type } => {
506+
crate::persistence::DraftAttachment::Image {
507+
content,
508+
mime_type,
509+
..
510+
} => {
507511
new_message.add_image_block(&mime_type, &content, cx);
508512
}
509513
crate::persistence::DraftAttachment::Text { content } => {

0 commit comments

Comments
 (0)