Skip to content

Commit d8c70b4

Browse files
authored
Merge pull request #272 from syncable-dev/develop
feat: small fixes, truncation for docker output, default bedrock mode…
2 parents d7834a7 + 33dd33e commit d8c70b4

5 files changed

Lines changed: 186 additions & 82 deletions

File tree

src/agent/session.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ impl ChatSession {
186186
let default_model = match provider {
187187
ProviderType::OpenAI => "gpt-5.2".to_string(),
188188
ProviderType::Anthropic => "claude-sonnet-4-5-20250929".to_string(),
189-
ProviderType::Bedrock => "global.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string(),
189+
ProviderType::Bedrock => "global.anthropic.claude-sonnet-4-20250514-v1:0".to_string(),
190190
};
191191

192192
Self {

src/agent/ui/hooks.rs

Lines changed: 71 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ use tokio::sync::Mutex;
1919
/// Maximum lines to show in preview before collapsing
2020
const PREVIEW_LINES: usize = 4;
2121

22+
/// Safely truncate a string to a maximum character count, handling UTF-8 properly.
23+
/// Adds "..." suffix when truncation occurs.
24+
fn truncate_safe(s: &str, max_chars: usize) -> String {
25+
let char_count = s.chars().count();
26+
if char_count <= max_chars {
27+
s.to_string()
28+
} else {
29+
let truncate_to = max_chars.saturating_sub(3);
30+
let truncated: String = s.chars().take(truncate_to).collect();
31+
format!("{}...", truncated)
32+
}
33+
}
34+
2235
/// Tool call state with full output for expansion
2336
#[derive(Debug, Clone)]
2437
pub struct ToolCallState {
@@ -620,6 +633,34 @@ fn print_tool_result(name: &str, args: &str, result: &str) -> (bool, Vec<String>
620633
}
621634
});
622635

636+
// If parsing failed, check if it's a tool error message
637+
// Tool errors come through as plain strings like "Shell error: ..."
638+
let parsed = if parsed.is_err() && !result.is_empty() {
639+
// Check for common error patterns
640+
let is_tool_error = result.contains("error:")
641+
|| result.contains("Error:")
642+
|| result.starts_with("Shell error")
643+
|| result.starts_with("Toolset error")
644+
|| result.starts_with("ToolCallError");
645+
646+
if is_tool_error {
647+
// Wrap the error message in a JSON structure so formatters can handle it
648+
let clean_msg = result
649+
.replace("Toolset error: ", "")
650+
.replace("ToolCallError: ", "")
651+
.replace("Shell error: ", "");
652+
Ok(serde_json::json!({
653+
"error": true,
654+
"message": clean_msg,
655+
"success": false
656+
}))
657+
} else {
658+
parsed
659+
}
660+
} else {
661+
parsed
662+
};
663+
623664
// Format output based on tool type
624665
let (status_ok, output_lines) = match name {
625666
"shell" => format_shell_result(&parsed),
@@ -790,6 +831,19 @@ fn format_shell_result(
790831
parsed: &Result<serde_json::Value, serde_json::Error>,
791832
) -> (bool, Vec<String>) {
792833
if let Ok(v) = parsed {
834+
// Check if this is an error message (from tool error or blocked command)
835+
if let Some(error_msg) = v.get("message").and_then(|m| m.as_str()) {
836+
if v.get("error").and_then(|e| e.as_bool()).unwrap_or(false) {
837+
return (false, vec![error_msg.to_string()]);
838+
}
839+
}
840+
841+
// Check for cancelled or blocked operations (plan mode, user cancel)
842+
if v.get("cancelled").and_then(|c| c.as_bool()).unwrap_or(false) {
843+
let reason = v.get("reason").and_then(|r| r.as_str()).unwrap_or("cancelled");
844+
return (false, vec![reason.to_string()]);
845+
}
846+
793847
let success = v.get("success").and_then(|s| s.as_bool()).unwrap_or(false);
794848
let stdout = v.get("stdout").and_then(|s| s.as_str()).unwrap_or("");
795849
let stderr = v.get("stderr").and_then(|s| s.as_str()).unwrap_or("");
@@ -1185,11 +1239,7 @@ fn format_hadolint_result(
11851239
if let Some(quick_fixes) = v.get("quick_fixes").and_then(|q| q.as_array())
11861240
&& let Some(first_fix) = quick_fixes.first().and_then(|f| f.as_str())
11871241
{
1188-
let truncated = if first_fix.len() > 70 {
1189-
format!("{}...", &first_fix[..67])
1190-
} else {
1191-
first_fix.to_string()
1192-
};
1242+
let truncated = truncate_safe(first_fix, 70);
11931243
lines.push(format!(
11941244
"{} → Fix: {}{}",
11951245
ansi::INFO_BLUE,
@@ -1233,11 +1283,7 @@ fn format_hadolint_issue(issue: &serde_json::Value, icon: &str, color: &str) ->
12331283
};
12341284

12351285
// Truncate message
1236-
let msg_display = if message.len() > 50 {
1237-
format!("{}...", &message[..47])
1238-
} else {
1239-
message.to_string()
1240-
};
1286+
let msg_display = truncate_safe(message, 50);
12411287

12421288
format!(
12431289
"{}{} L{}:{} {}{}[{}]{} {} {}",
@@ -1284,11 +1330,7 @@ fn format_kubelint_result(
12841330
));
12851331
for (i, err) in errors.iter().take(3).enumerate() {
12861332
if let Some(err_str) = err.as_str() {
1287-
let truncated = if err_str.len() > 70 {
1288-
format!("{}...", &err_str[..67])
1289-
} else {
1290-
err_str.to_string()
1291-
};
1333+
let truncated = truncate_safe(err_str, 70);
12921334
lines.push(format!(
12931335
"{} {} {}{}",
12941336
ansi::HIGH,
@@ -1420,11 +1462,7 @@ fn format_kubelint_result(
14201462
if let Some(quick_fixes) = v.get("quick_fixes").and_then(|q| q.as_array())
14211463
&& let Some(first_fix) = quick_fixes.first().and_then(|f| f.as_str())
14221464
{
1423-
let truncated = if first_fix.len() > 70 {
1424-
format!("{}...", &first_fix[..67])
1425-
} else {
1426-
first_fix.to_string()
1427-
};
1465+
let truncated = truncate_safe(first_fix, 70);
14281466
lines.push(format!(
14291467
"{} → Fix: {}{}",
14301468
ansi::INFO_BLUE,
@@ -1450,8 +1488,6 @@ fn format_kubelint_result(
14501488
(false, vec!["kubelint analysis complete".to_string()])
14511489
}
14521490
}
1453-
1454-
/// Format a single kubelint issue for display
14551491
fn format_kubelint_issue(issue: &serde_json::Value, icon: &str, color: &str) -> String {
14561492
let check = issue.get("check").and_then(|c| c.as_str()).unwrap_or("?");
14571493
let message = issue.get("message").and_then(|m| m.as_str()).unwrap_or("?");
@@ -1468,11 +1504,7 @@ fn format_kubelint_issue(issue: &serde_json::Value, icon: &str, color: &str) ->
14681504
};
14691505

14701506
// Truncate message
1471-
let msg_display = if message.len() > 50 {
1472-
format!("{}...", &message[..47])
1473-
} else {
1474-
message.to_string()
1475-
};
1507+
let msg_display = truncate_safe(message, 50);
14761508

14771509
format!(
14781510
"{}{} L{}:{} {}{}[{}]{} {} {}",
@@ -1519,11 +1551,7 @@ fn format_helmlint_result(
15191551
));
15201552
for (i, err) in errors.iter().take(3).enumerate() {
15211553
if let Some(err_str) = err.as_str() {
1522-
let truncated = if err_str.len() > 70 {
1523-
format!("{}...", &err_str[..67])
1524-
} else {
1525-
err_str.to_string()
1526-
};
1554+
let truncated = truncate_safe(err_str, 70);
15271555
lines.push(format!(
15281556
"{} {} {}{}",
15291557
ansi::HIGH,
@@ -1655,11 +1683,7 @@ fn format_helmlint_result(
16551683
if let Some(quick_fixes) = v.get("quick_fixes").and_then(|q| q.as_array())
16561684
&& let Some(first_fix) = quick_fixes.first().and_then(|f| f.as_str())
16571685
{
1658-
let truncated = if first_fix.len() > 70 {
1659-
format!("{}...", &first_fix[..67])
1660-
} else {
1661-
first_fix.to_string()
1662-
};
1686+
let truncated = truncate_safe(first_fix, 70);
16631687
lines.push(format!(
16641688
"{} → Fix: {}{}",
16651689
ansi::INFO_BLUE,
@@ -1704,18 +1728,15 @@ fn format_helmlint_issue(issue: &serde_json::Value, icon: &str, color: &str) ->
17041728
};
17051729

17061730
// Short file name
1707-
let file_short = if file.len() > 20 {
1708-
format!("...{}", &file[file.len().saturating_sub(17)..])
1731+
let file_short = if file.chars().count() > 20 {
1732+
let skip = file.chars().count().saturating_sub(17);
1733+
format!("...{}", file.chars().skip(skip).collect::<String>())
17091734
} else {
17101735
file.to_string()
17111736
};
17121737

17131738
// Truncate message
1714-
let msg_display = if message.len() > 40 {
1715-
format!("{}...", &message[..37])
1716-
} else {
1717-
message.to_string()
1718-
};
1739+
let msg_display = truncate_safe(message, 40);
17191740

17201741
format!(
17211742
"{}{} {}:{}:{} {}{}[{}]{} {} {}",
@@ -1923,11 +1944,7 @@ fn format_result_preview(result: &serde_json::Value) -> String {
19231944
.and_then(|v| v.as_str())
19241945
.unwrap_or("");
19251946

1926-
let detail_short = if detail.len() > 40 {
1927-
format!("{}...", &detail[..37])
1928-
} else {
1929-
detail.to_string()
1930-
};
1947+
let detail_short = truncate_safe(detail, 40);
19311948

19321949
if detail_short.is_empty() {
19331950
name.to_string()
@@ -1969,8 +1986,10 @@ fn tool_to_focus(tool_name: &str, args: &str) -> Option<String> {
19691986
"read_file" | "write_file" => {
19701987
parsed.get("path").and_then(|p| p.as_str()).map(|p| {
19711988
// Shorten long paths
1972-
if p.len() > 50 {
1973-
format!("...{}", &p[p.len().saturating_sub(47)..])
1989+
let char_count = p.chars().count();
1990+
if char_count > 50 {
1991+
let skip = char_count.saturating_sub(47);
1992+
format!("...{}", p.chars().skip(skip).collect::<String>())
19741993
} else {
19751994
p.to_string()
19761995
}
@@ -1982,11 +2001,7 @@ fn tool_to_focus(tool_name: &str, args: &str) -> Option<String> {
19822001
.map(|p| p.to_string()),
19832002
"shell" => parsed.get("command").and_then(|c| c.as_str()).map(|cmd| {
19842003
// Truncate long commands
1985-
if cmd.len() > 60 {
1986-
format!("{}...", &cmd[..57])
1987-
} else {
1988-
cmd.to_string()
1989-
}
2004+
truncate_safe(cmd, 60)
19902005
}),
19912006
"hadolint" | "dclint" | "kubelint" | "helmlint" => parsed
19922007
.get("path")

0 commit comments

Comments
 (0)