Skip to content

Commit b640c13

Browse files
feat: display errors on cli (#123)
Co-authored-by: Tushar Mathur <tusharmath@gmail.com>
1 parent 10189cd commit b640c13

8 files changed

+222
-163
lines changed

crates/forge_main/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ pub mod status;
55

66
pub use console::CONSOLE;
77
pub use input::UserInput;
8-
pub use status::{StatusDisplay, StatusKind};
8+
pub use status::StatusDisplay;

crates/forge_main/src/main.rs

+55-73
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
use anyhow::Result;
2-
use chrono::Local;
32
use clap::Parser;
43
use forge_domain::{ChatRequest, ChatResponse, ModelId};
5-
use forge_main::{StatusDisplay, StatusKind, UserInput, CONSOLE};
4+
use forge_main::{StatusDisplay, UserInput, CONSOLE};
65
use forge_server::API;
76
use tokio_stream::StreamExt;
87

@@ -13,10 +12,6 @@ struct Cli {
1312
verbose: bool,
1413
}
1514

16-
fn get_timestamp() -> String {
17-
Local::now().format("%H:%M:%S%.3f").to_string()
18-
}
19-
2015
#[tokio::main]
2116
async fn main() -> Result<()> {
2217
let cli = Cli::parse();
@@ -49,75 +44,62 @@ async fn main() -> Result<()> {
4944
conversation_id: current_conversation_id,
5045
};
5146

52-
let mut stream = api
53-
.chat(chat)
54-
.await
55-
.map_err(|e| anyhow::anyhow!("Failed to start chat stream: {}", e))?;
56-
57-
while let Some(message) = stream.next().await {
58-
let message = message.map_err(|e| anyhow::anyhow!("Stream error: {}", e))?;
59-
match message {
60-
ChatResponse::Text(text) => {
61-
CONSOLE.write(&text)?;
62-
}
63-
ChatResponse::ToolCallDetected(_) => {}
64-
ChatResponse::ToolCallArgPart(arg) => {
65-
if cli.verbose {
66-
CONSOLE.write(&arg)?;
67-
}
68-
}
69-
ChatResponse::ToolCallStart(tool_call_full) => {
70-
let tool_name = tool_call_full.name.as_str();
71-
let status = StatusDisplay {
72-
kind: StatusKind::Execute,
73-
message: tool_name,
74-
timestamp: Some(get_timestamp()),
75-
error_details: None,
76-
};
77-
CONSOLE.newline()?;
78-
CONSOLE.writeln(status.format())?;
79-
}
80-
ChatResponse::ToolCallEnd(tool_result) => {
81-
if cli.verbose {
82-
CONSOLE.writeln(tool_result.to_string())?;
83-
}
84-
let tool_name = tool_result.name.as_str();
85-
let status = if tool_result.is_error {
86-
StatusDisplay {
87-
kind: StatusKind::Failed,
88-
message: tool_name,
89-
timestamp: Some(get_timestamp()),
90-
error_details: Some("error"),
91-
}
92-
} else {
93-
StatusDisplay {
94-
kind: StatusKind::Success,
95-
message: tool_name,
96-
timestamp: Some(get_timestamp()),
97-
error_details: None,
47+
match api.chat(chat).await {
48+
Ok(mut stream) => {
49+
while let Some(message) = stream.next().await {
50+
match message {
51+
Ok(message) => match message {
52+
ChatResponse::Text(text) => {
53+
CONSOLE.write(&text)?;
54+
}
55+
ChatResponse::ToolCallDetected(_) => {}
56+
ChatResponse::ToolCallArgPart(arg) => {
57+
if cli.verbose {
58+
CONSOLE.write(&arg)?;
59+
}
60+
}
61+
ChatResponse::ToolCallStart(tool_call_full) => {
62+
let tool_name = tool_call_full.name.as_str();
63+
CONSOLE.newline()?;
64+
CONSOLE.writeln(StatusDisplay::execute(tool_name).format())?;
65+
}
66+
ChatResponse::ToolCallEnd(tool_result) => {
67+
if cli.verbose {
68+
CONSOLE.writeln(tool_result.to_string())?;
69+
}
70+
let tool_name = tool_result.name.as_str();
71+
let status = if tool_result.is_error {
72+
StatusDisplay::failed(tool_name)
73+
} else {
74+
StatusDisplay::success(tool_name)
75+
};
76+
CONSOLE.write(status.format())?;
77+
}
78+
ChatResponse::ConversationStarted(conversation_id) => {
79+
current_conversation_id = Some(conversation_id);
80+
}
81+
ChatResponse::ModifyContext(_) => {}
82+
ChatResponse::Complete => {}
83+
ChatResponse::Error(err) => {
84+
CONSOLE.writeln(StatusDisplay::failed(err.to_string()).format())?;
85+
}
86+
ChatResponse::PartialTitle(_) => {}
87+
ChatResponse::CompleteTitle(title) => {
88+
CONSOLE.writeln(StatusDisplay::title(title).format())?;
89+
}
90+
ChatResponse::FinishReason(_) => {}
91+
},
92+
Err(err) => {
93+
CONSOLE.writeln(StatusDisplay::failed(err.to_string()).format())?;
9894
}
99-
};
100-
CONSOLE.write(status.format())?;
101-
}
102-
ChatResponse::ConversationStarted(conversation_id) => {
103-
current_conversation_id = Some(conversation_id);
104-
}
105-
ChatResponse::ModifyContext(_) => {}
106-
ChatResponse::Complete => {}
107-
ChatResponse::Error(err) => {
108-
return Err(anyhow::anyhow!("Chat error: {:?}", err));
109-
}
110-
ChatResponse::PartialTitle(_) => {}
111-
ChatResponse::CompleteTitle(title) => {
112-
let status = StatusDisplay {
113-
kind: StatusKind::Title,
114-
message: &title,
115-
timestamp: Some(get_timestamp()),
116-
error_details: None,
117-
};
118-
CONSOLE.writeln(status.format())?;
95+
}
11996
}
120-
ChatResponse::FinishReason(_) => {}
97+
}
98+
Err(err) => {
99+
CONSOLE.writeln(
100+
StatusDisplay::failed_with(err.to_string(), "Failed to establish chat stream")
101+
.format(),
102+
)?;
121103
}
122104
}
123105

crates/forge_main/src/status.rs

+76-32
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,102 @@
11
use colored::Colorize;
22

3-
pub enum StatusKind {
3+
#[derive(Clone)]
4+
enum Kind {
45
Execute,
56
Success,
67
Failed,
78
Title,
89
}
910

10-
impl StatusKind {
11+
impl Kind {
1112
fn icon(&self) -> &'static str {
1213
match self {
13-
StatusKind::Execute => "⚙",
14-
StatusKind::Success => "✓",
15-
StatusKind::Failed => "✗",
16-
StatusKind::Title => "◆",
14+
Kind::Execute => "⚙",
15+
Kind::Success => "✓",
16+
Kind::Failed => "✗",
17+
Kind::Title => "◆",
1718
}
1819
}
1920

2021
fn label(&self) -> &'static str {
2122
match self {
22-
StatusKind::Execute => "EXECUTE",
23-
StatusKind::Success => "SUCCESS",
24-
StatusKind::Failed => "FAILED",
25-
StatusKind::Title => "TITLE",
23+
Kind::Execute => "EXECUTE",
24+
Kind::Success => "SUCCESS",
25+
Kind::Failed => "FAILED",
26+
Kind::Title => "TITLE",
2627
}
2728
}
2829
}
2930

30-
pub struct StatusDisplay<'a> {
31-
pub kind: StatusKind,
32-
pub message: &'a str,
33-
pub timestamp: Option<String>,
34-
pub error_details: Option<&'a str>,
31+
#[derive(Clone)]
32+
pub struct StatusDisplay {
33+
kind: Kind,
34+
message: String,
35+
error_details: Option<String>,
3536
}
3637

37-
impl StatusDisplay<'_> {
38+
impl StatusDisplay {
39+
/// Create a status for executing a tool
40+
pub fn execute(message: impl Into<String>) -> Self {
41+
Self {
42+
kind: Kind::Execute,
43+
message: message.into(),
44+
error_details: None,
45+
}
46+
}
47+
48+
/// Create a success status
49+
pub fn success(message: impl Into<String>) -> Self {
50+
Self {
51+
kind: Kind::Success,
52+
message: message.into(),
53+
error_details: None,
54+
}
55+
}
56+
57+
/// Create a failure status
58+
pub fn failed(message: impl Into<String>) -> Self {
59+
Self {
60+
kind: Kind::Failed,
61+
message: message.into(),
62+
error_details: None,
63+
}
64+
}
65+
66+
/// Create a failure status with additional details
67+
pub fn failed_with(message: impl Into<String>, details: impl Into<String>) -> Self {
68+
Self {
69+
kind: Kind::Failed,
70+
message: message.into(),
71+
error_details: Some(details.into()),
72+
}
73+
}
74+
75+
/// Create a title status
76+
pub fn title(message: impl Into<String>) -> Self {
77+
Self {
78+
kind: Kind::Title,
79+
message: message.into(),
80+
error_details: None,
81+
}
82+
}
83+
3884
pub fn format(&self) -> String {
3985
let (icon, label, message) = match self.kind {
40-
StatusKind::Execute => (
86+
Kind::Execute => (
4187
self.icon().cyan(),
4288
self.label().bold().cyan(),
4389
format!("{} ...", self.message.bold().cyan()),
4490
),
45-
StatusKind::Success => (
91+
Kind::Success => (
4692
self.icon().green(),
4793
self.label().bold().green(),
4894
self.message.bold().green().to_string(),
4995
),
50-
StatusKind::Failed => {
96+
Kind::Failed => {
5197
let error_suffix = self
5298
.error_details
99+
.as_ref()
53100
.map(|e| format!(" ({})", e))
54101
.unwrap_or_default();
55102
(
@@ -58,25 +105,22 @@ impl StatusDisplay<'_> {
58105
format!("{}{}", self.message.bold().red(), error_suffix.red()),
59106
)
60107
}
61-
StatusKind::Title => (
108+
Kind::Title => (
62109
self.icon().blue(),
63110
self.label().bold().blue(),
64111
self.message.bold().blue().to_string(),
65112
),
66113
};
67114

68-
if let Some(timestamp) = &self.timestamp {
69-
format!(
70-
"{} {} {} {} {}",
71-
timestamp.dimmed(),
72-
icon,
73-
label,
74-
"▶".bold(),
75-
message
76-
)
77-
} else {
78-
format!("{} {} {} {}", icon, label, "▶".bold(), message)
79-
}
115+
let timestamp = chrono::Local::now().format("%H:%M:%S%.3f").to_string();
116+
format!(
117+
"{} {} {} {} {}",
118+
timestamp.dimmed(),
119+
icon,
120+
label,
121+
"▶".bold(),
122+
message
123+
)
80124
}
81125

82126
fn icon(&self) -> &'static str {

crates/forge_server/src/prompts/coding/system.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Critical Rules:
3535
4. Always provide clear and concise explanations for your actions.
3636
5. Always return raw text with original special characters.
3737
6. Confirm with the user before deleting existing tests if they are failing.
38-
7. Always run tests to validate your changes before asking the user for feedback.
38+
7. Always validate your changes by running tests.
3939
8. Always execute shell commands in non-interactive mode to ensure fail-fast behavior, preventing any user input prompts or execution delays.
4040
9. Leverage your tools to take feedback from the user and improve your responses.
4141

Original file line numberDiff line numberDiff line change
@@ -1,27 +1,43 @@
1-
You are Code-Forge's Title Generation Expert. Your task is to analyze content and generate precise, impactful titles that capture the essence of the material.
2-
3-
Input Format:
4-
Content will be provided within <content> XML tags.
5-
6-
Requirements:
7-
1. Maximum 10 words
8-
2. Capture core message or functionality
9-
3. Use clear, technical language
10-
4. Avoid unnecessary words or marketing language
11-
5. Return ONLY the title, without any tags or explanations
12-
13-
Analysis Process:
14-
1. Identify main technical concepts
15-
2. Extract key functionality or purpose
16-
3. Determine target audience
17-
4. Synthesize into concise title
18-
19-
Example:
20-
input:
21-
<content>
22-
A function that processes log files, extracts error messages, and generates daily reports.
23-
</content>
24-
output:
25-
Log Processing Pipeline for Automated Error Report Generation
26-
27-
Note: Focus on technical accuracy and clarity. Respond with ONLY the title text.
1+
You are Code-Forge's Title Generation Expert, tasked with analyzing technical content and generating precise, impactful titles that capture the essence of the material. Your goal is to create titles that are clear, informative, and tailored for a technical audience.
2+
3+
Please follow these steps to generate an appropriate title:
4+
5+
1. Analyze the content carefully, identifying the main technical concepts, key functionality, and purpose.
6+
2. Determine the likely target audience for this content.
7+
3. Synthesize your analysis into a concise title that meets the following requirements:
8+
- Between 5 and 10 words in length
9+
- Captures the core message or functionality
10+
- Uses clear, technical language
11+
- Avoids unnecessary words or marketing language
12+
13+
Before providing your final title, wrap your thought process in <title_generation_process> tags. Follow these steps:
14+
15+
1. List the main technical concepts you've identified.
16+
2. Describe the key functionality or purpose of the content.
17+
3. Specify the likely target audience.
18+
4. Generate 3-5 potential titles that meet the requirements.
19+
5. For each potential title, count the number of words by listing each word with a number (e.g., 1. Title 2. Word 3. Count).
20+
6. Evaluate each title based on how well it captures the core message and uses appropriate technical language.
21+
7. Select the best title and explain your choice.
22+
23+
After your analysis, provide only the final title without any additional explanation or tags.
24+
25+
Example output structure:
26+
27+
<title_generation_process>
28+
29+
1. Main technical concepts: [List identified concepts]
30+
2. Key functionality: [Describe the primary function]
31+
3. Target audience: [Specify the likely audience]
32+
4. Potential titles:
33+
- Title 1: [List with word count]
34+
- Title 2: [List with word count]
35+
- Title 3: [List with word count]
36+
5. Evaluation: [Brief evaluation of each title]
37+
6. Selected title: [Explain your choice]
38+
39+
</title_generation_process>
40+
41+
[Your final title here, between 5-10 words]
42+
43+
Remember, the final output should only contain the title itself, without any formatting or explanation.

0 commit comments

Comments
 (0)