Skip to content

Commit 041b86c

Browse files
nikomatsakiskensave
authored andcommitted
Proposal: Add a /reply command (#2680)
* Add /reply command to quote last assistant message in editor - Made open_editor public in editor.rs for reuse - Added ReplyArgs and reply module to CLI structure - Quotes most recent assistant message with > prefixes - Handles edge cases: no message found, empty content - Provides colored user feedback throughout flow * Add /reply to COMMANDS array for autocompletion Fixes missing tab completion for the /reply command by adding it to the COMMANDS list used by the rustyline completer. * Fix /reply to detect unchanged content When user opens editor with quoted message and quits without changes, detect that content is unchanged from initial text and don't submit. Updates message to 'No changes made in editor' for clarity. * missing docs
1 parent d03b01d commit 041b86c

File tree

3 files changed

+108
-0
lines changed

3 files changed

+108
-0
lines changed

crates/chat-cli/src/cli/chat/cli/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod model;
1212
pub mod persist;
1313
pub mod profile;
1414
pub mod prompts;
15+
pub mod reply;
1516
pub mod subscribe;
1617
pub mod tangent;
1718
pub mod todos;
@@ -32,6 +33,7 @@ use model::ModelArgs;
3233
use persist::PersistSubcommand;
3334
use profile::AgentSubcommand;
3435
use prompts::PromptsArgs;
36+
use reply::ReplyArgs;
3537
use tangent::TangentArgs;
3638
use todos::TodoSubcommand;
3739
use tools::ToolsArgs;
@@ -73,6 +75,8 @@ pub enum SlashCommand {
7375
/// Open $EDITOR (defaults to vi) to compose a prompt
7476
#[command(name = "editor")]
7577
PromptEditor(EditorArgs),
78+
/// Open $EDITOR with the most recent assistant message quoted for reply
79+
Reply(ReplyArgs),
7680
/// Summarize the conversation to free up context space
7781
Compact(CompactArgs),
7882
/// View tools and permissions
@@ -143,6 +147,7 @@ impl SlashCommand {
143147
Self::Context(args) => args.execute(os, session).await,
144148
Self::Knowledge(subcommand) => subcommand.execute(os, session).await,
145149
Self::PromptEditor(args) => args.execute(session).await,
150+
Self::Reply(args) => args.execute(session).await,
146151
Self::Compact(args) => args.execute(os, session).await,
147152
Self::Tools(args) => args.execute(session).await,
148153
Self::Issue(args) => {
@@ -187,6 +192,7 @@ impl SlashCommand {
187192
Self::Context(_) => "context",
188193
Self::Knowledge(_) => "knowledge",
189194
Self::PromptEditor(_) => "editor",
195+
Self::Reply(_) => "reply",
190196
Self::Compact(_) => "compact",
191197
Self::Tools(_) => "tools",
192198
Self::Issue(_) => "issue",
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use clap::Args;
2+
use crossterm::execute;
3+
use crossterm::style::{self, Color};
4+
5+
use super::editor::open_editor;
6+
use crate::cli::chat::{ChatError, ChatSession, ChatState};
7+
8+
/// Arguments to the `/reply` command.
9+
#[deny(missing_docs)]
10+
#[derive(Debug, PartialEq, Args)]
11+
pub struct ReplyArgs {}
12+
13+
impl ReplyArgs {
14+
pub async fn execute(self, session: &mut ChatSession) -> Result<ChatState, ChatError> {
15+
// Get the most recent assistant message from transcript
16+
let last_assistant_message = session
17+
.conversation
18+
.transcript
19+
.iter()
20+
.rev()
21+
.find(|msg| !msg.starts_with("> "))
22+
.cloned();
23+
24+
let initial_text = match last_assistant_message {
25+
Some(msg) => {
26+
// Format with > prefix for each line
27+
msg.lines()
28+
.map(|line| format!("> {}", line))
29+
.collect::<Vec<_>>()
30+
.join("\n")
31+
},
32+
None => {
33+
execute!(
34+
session.stderr,
35+
style::SetForegroundColor(Color::Yellow),
36+
style::Print("\nNo assistant message found to reply to.\n\n"),
37+
style::SetForegroundColor(Color::Reset)
38+
)?;
39+
40+
return Ok(ChatState::PromptUser {
41+
skip_printing_tools: true,
42+
});
43+
},
44+
};
45+
46+
let content = match open_editor(Some(initial_text.clone())) {
47+
Ok(content) => content,
48+
Err(err) => {
49+
execute!(
50+
session.stderr,
51+
style::SetForegroundColor(Color::Red),
52+
style::Print(format!("\nError opening editor: {}\n\n", err)),
53+
style::SetForegroundColor(Color::Reset)
54+
)?;
55+
56+
return Ok(ChatState::PromptUser {
57+
skip_printing_tools: true,
58+
});
59+
},
60+
};
61+
62+
Ok(
63+
match content.trim().is_empty() || content.trim() == initial_text.trim() {
64+
true => {
65+
execute!(
66+
session.stderr,
67+
style::SetForegroundColor(Color::Yellow),
68+
style::Print("\nNo changes made in editor, not submitting.\n\n"),
69+
style::SetForegroundColor(Color::Reset)
70+
)?;
71+
72+
ChatState::PromptUser {
73+
skip_printing_tools: true,
74+
}
75+
},
76+
false => {
77+
execute!(
78+
session.stderr,
79+
style::SetForegroundColor(Color::Green),
80+
style::Print("\nContent loaded from editor. Submitting prompt...\n\n"),
81+
style::SetForegroundColor(Color::Reset)
82+
)?;
83+
84+
// Display the content as if the user typed it
85+
execute!(
86+
session.stderr,
87+
style::SetAttribute(style::Attribute::Reset),
88+
style::SetForegroundColor(Color::Magenta),
89+
style::Print("> "),
90+
style::SetAttribute(style::Attribute::Reset),
91+
style::Print(&content),
92+
style::Print("\n")
93+
)?;
94+
95+
// Process the content as user input
96+
ChatState::HandleInput { input: content }
97+
},
98+
},
99+
)
100+
}
101+
}

crates/chat-cli/src/cli/chat/prompt.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pub const COMMANDS: &[&str] = &[
5454
"/clear",
5555
"/help",
5656
"/editor",
57+
"/reply",
5758
"/issue",
5859
"/quit",
5960
"/tools",

0 commit comments

Comments
 (0)