Skip to content

Commit a978fff

Browse files
authored
Merge pull request #257 from syncable-dev/develop
Develop
2 parents 4f89905 + 6dce0e6 commit a978fff

68 files changed

Lines changed: 3413 additions & 592 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ term_size = "0.3"
6060
# Vulnerability checking dependencies
6161
rustsec = "0.30"
6262
reqwest = { version = "0.12", features = ["json", "blocking"] }
63-
tokio = { version = "1", features = ["rt", "macros", "rt-multi-thread", "sync", "process", "io-util"] }
63+
tokio = { version = "1", features = ["rt", "macros", "rt-multi-thread", "sync", "process", "io-util", "signal"] }
6464
textwrap = "0.16"
6565
tempfile = "3"
6666
dirs = "6"

src/agent/commands.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ pub const SLASH_COMMANDS: &[SlashCommand] = &[
7878
description: "Show incomplete plans and continue",
7979
auto_execute: true,
8080
},
81+
SlashCommand {
82+
name: "resume",
83+
alias: Some("s"),
84+
description: "Browse and resume previous sessions",
85+
auto_execute: true,
86+
},
87+
SlashCommand {
88+
name: "sessions",
89+
alias: Some("ls"),
90+
description: "List available sessions for this project",
91+
auto_execute: true,
92+
},
8193
SlashCommand {
8294
name: "exit",
8395
alias: Some("q"),

src/agent/compact/strategy.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ impl CompactionStrategy {
169169
// We're evicting a tool call - need to also evict its result
170170
// Find the tool result with matching ID
171171
if let Some(tool_id) = &last_evicted.tool_id {
172-
for i in end..messages.len().min(end + 5) {
173-
if messages[i].is_tool_result && messages[i].tool_id.as_ref() == Some(tool_id) {
172+
for (i, msg) in messages.iter().enumerate().skip(end).take(5) {
173+
if msg.is_tool_result && msg.tool_id.as_ref() == Some(tool_id) {
174174
// Found matching result - extend eviction to include it
175175
end = i + 1;
176176
break;

src/agent/mod.rs

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub mod commands;
3434
pub mod compact;
3535
pub mod history;
3636
pub mod ide;
37+
pub mod persistence;
3738
pub mod prompts;
3839
pub mod session;
3940
pub mod tools;
@@ -117,7 +118,7 @@ fn get_system_prompt(project_path: &Path, query: Option<&str>, plan_mode: PlanMo
117118
}
118119
// Then check if it's DevOps generation (Docker, Terraform, Helm)
119120
if prompts::is_generation_query(q) {
120-
return prompts::get_devops_prompt(project_path);
121+
return prompts::get_devops_prompt(project_path, Some(q));
121122
}
122123
}
123124
// Default to analysis prompt
@@ -134,6 +135,10 @@ pub async fn run_interactive(
134135

135136
let mut session = ChatSession::new(project_path, provider, model);
136137

138+
// Terminal layout for split screen is disabled for now - see notes below
139+
// let terminal_layout = ui::TerminalLayout::new();
140+
// let layout_state = terminal_layout.state();
141+
137142
// Initialize conversation history with compaction support
138143
let mut conversation_history = ConversationHistory::new();
139144

@@ -176,6 +181,19 @@ pub async fn run_interactive(
176181

177182
session.print_banner();
178183

184+
// NOTE: Terminal layout with ANSI scroll regions is disabled for now.
185+
// The scroll region approach conflicts with the existing input/output flow.
186+
// TODO: Implement proper scroll region support that integrates with the input handler.
187+
// For now, we rely on the pause/resume mechanism in progress indicator.
188+
//
189+
// if let Err(e) = terminal_layout.init() {
190+
// eprintln!(
191+
// "{}",
192+
// format!("Note: Terminal layout initialization failed: {}. Using fallback mode.", e)
193+
// .dimmed()
194+
// );
195+
// }
196+
179197
// Raw Rig messages for multi-turn - preserves Reasoning blocks for thinking
180198
// Our ConversationHistory only stores text summaries, but rig needs full Message structure
181199
let mut raw_chat_history: Vec<rig::completion::Message> = Vec::new();
@@ -185,6 +203,9 @@ pub async fn run_interactive(
185203
// Auto-accept mode for plan execution (skips write confirmations)
186204
let mut auto_accept_writes = false;
187205

206+
// Initialize session recorder for conversation persistence
207+
let mut session_recorder = persistence::SessionRecorder::new(project_path);
208+
188209
loop {
189210
// Show conversation status if we have history
190211
if !conversation_history.is_empty() {
@@ -323,6 +344,12 @@ pub async fn run_interactive(
323344
// Create hook for Claude Code style tool display
324345
let hook = ToolDisplayHook::new();
325346

347+
// Create progress indicator for visual feedback during generation
348+
let progress = ui::GenerationIndicator::new();
349+
// Layout connection disabled - using inline progress mode
350+
// progress.state().set_layout(layout_state.clone());
351+
hook.set_progress_state(progress.state()).await;
352+
326353
let project_path_buf = session.project_path.clone();
327354
// Select prompt based on query type (analysis vs generation) and plan mode
328355
let preamble = get_system_prompt(
@@ -336,7 +363,24 @@ pub async fn run_interactive(
336363
// Note: using raw_chat_history directly which preserves Reasoning blocks
337364
// This is needed for extended thinking to work with multi-turn conversations
338365

339-
let response = match session.provider {
366+
// Get progress state for interrupt detection
367+
let progress_state = progress.state();
368+
369+
// Use tokio::select! to race the API call against Ctrl+C
370+
// This allows immediate cancellation, not just between tool calls
371+
let mut user_interrupted = false;
372+
373+
// API call with Ctrl+C interrupt support
374+
let response = tokio::select! {
375+
biased; // Check ctrl_c first for faster response
376+
377+
_ = tokio::signal::ctrl_c() => {
378+
user_interrupted = true;
379+
Err::<String, String>("User cancelled".to_string())
380+
}
381+
382+
result = async {
383+
match session.provider {
340384
ProviderType::OpenAI => {
341385
let client = openai::Client::from_env();
342386
// For GPT-5.x reasoning models, enable reasoning with summary output
@@ -368,7 +412,8 @@ pub async fn run_interactive(
368412
.tool(TerraformValidateTool::new(project_path_buf.clone()))
369413
.tool(TerraformInstallTool::new())
370414
.tool(ReadFileTool::new(project_path_buf.clone()))
371-
.tool(ListDirectoryTool::new(project_path_buf.clone()));
415+
.tool(ListDirectoryTool::new(project_path_buf.clone()))
416+
.tool(WebFetchTool::new());
372417

373418
// Add tools based on mode
374419
if is_planning {
@@ -446,7 +491,8 @@ pub async fn run_interactive(
446491
.tool(TerraformValidateTool::new(project_path_buf.clone()))
447492
.tool(TerraformInstallTool::new())
448493
.tool(ReadFileTool::new(project_path_buf.clone()))
449-
.tool(ListDirectoryTool::new(project_path_buf.clone()));
494+
.tool(ListDirectoryTool::new(project_path_buf.clone()))
495+
.tool(WebFetchTool::new());
450496

451497
// Add tools based on mode
452498
if is_planning {
@@ -528,7 +574,8 @@ pub async fn run_interactive(
528574
.tool(TerraformValidateTool::new(project_path_buf.clone()))
529575
.tool(TerraformInstallTool::new())
530576
.tool(ReadFileTool::new(project_path_buf.clone()))
531-
.tool(ListDirectoryTool::new(project_path_buf.clone()));
577+
.tool(ListDirectoryTool::new(project_path_buf.clone()))
578+
.tool(WebFetchTool::new());
532579

533580
// Add tools based on mode
534581
if is_planning {
@@ -579,9 +626,17 @@ pub async fn run_interactive(
579626
.with_hook(hook.clone())
580627
.multi_turn(50)
581628
.await
582-
}
629+
}
630+
}.map_err(|e| e.to_string())
631+
} => result
583632
};
584633

634+
// Stop the progress indicator before handling the response
635+
progress.stop().await;
636+
637+
// Suppress unused variable warnings
638+
let _ = (&progress_state, user_interrupted);
639+
585640
match response {
586641
Ok(text) => {
587642
// Show final response
@@ -663,6 +718,16 @@ pub async fn run_interactive(
663718
.history
664719
.push(("assistant".to_string(), text.clone()));
665720

721+
// Record to persistent session storage
722+
session_recorder.record_user_message(&input);
723+
session_recorder.record_assistant_message(&text, Some(&tool_calls));
724+
if let Err(e) = session_recorder.save() {
725+
eprintln!(
726+
"{}",
727+
format!(" Warning: Failed to save session: {}", e).dimmed()
728+
);
729+
}
730+
666731
// Check if plan_create was called - show interactive menu
667732
if let Some(plan_info) = find_plan_create_call(&tool_calls) {
668733
println!(); // Space before menu
@@ -714,6 +779,32 @@ pub async fn run_interactive(
714779

715780
println!();
716781

782+
// Check if this was a user-initiated cancellation (Ctrl+C)
783+
if err_str.contains("cancelled") || err_str.contains("Cancelled") {
784+
// Extract any completed work before cancellation
785+
let completed_tools = extract_tool_calls_from_hook(&hook).await;
786+
let tool_count = completed_tools.len();
787+
788+
eprintln!("{}", "⚠ Generation interrupted.".yellow());
789+
if tool_count > 0 {
790+
eprintln!(
791+
"{}",
792+
format!(" {} tool calls completed before interrupt.", tool_count)
793+
.dimmed()
794+
);
795+
// Add partial progress to history
796+
conversation_history.add_turn(
797+
current_input.clone(),
798+
format!("[Interrupted after {} tool calls]", tool_count),
799+
completed_tools,
800+
);
801+
}
802+
eprintln!("{}", " Type your next message to continue.".dimmed());
803+
804+
// Don't retry, don't mark as succeeded - just break to return to prompt
805+
break;
806+
}
807+
717808
// Check if this is a max depth error - handle as checkpoint
718809
if err_str.contains("MaxDepth")
719810
|| err_str.contains("max_depth")
@@ -1067,9 +1158,21 @@ pub async fn run_interactive(
10671158
println!();
10681159
}
10691160

1161+
// Clean up terminal layout before exiting (disabled - layout not initialized)
1162+
// if let Err(e) = terminal_layout.cleanup() {
1163+
// eprintln!(
1164+
// "{}",
1165+
// format!("Warning: Terminal cleanup failed: {}", e).dimmed()
1166+
// );
1167+
// }
1168+
10701169
Ok(())
10711170
}
10721171

1172+
// NOTE: wait_for_interrupt function removed - ESC interrupt feature disabled
1173+
// due to terminal corruption issues with spawn_blocking raw mode handling.
1174+
// TODO: Re-implement using tool hook callbacks for cleaner interruption.
1175+
10731176
/// Extract tool call records from the hook state for history tracking
10741177
async fn extract_tool_calls_from_hook(hook: &ToolDisplayHook) -> Vec<ToolCallRecord> {
10751178
let state = hook.state();
@@ -1422,7 +1525,8 @@ pub async fn run_query(
14221525
.tool(TerraformValidateTool::new(project_path_buf.clone()))
14231526
.tool(TerraformInstallTool::new())
14241527
.tool(ReadFileTool::new(project_path_buf.clone()))
1425-
.tool(ListDirectoryTool::new(project_path_buf.clone()));
1528+
.tool(ListDirectoryTool::new(project_path_buf.clone()))
1529+
.tool(WebFetchTool::new());
14261530

14271531
// Add generation tools if this is a generation query
14281532
if is_generation {
@@ -1467,7 +1571,8 @@ pub async fn run_query(
14671571
.tool(TerraformValidateTool::new(project_path_buf.clone()))
14681572
.tool(TerraformInstallTool::new())
14691573
.tool(ReadFileTool::new(project_path_buf.clone()))
1470-
.tool(ListDirectoryTool::new(project_path_buf.clone()));
1574+
.tool(ListDirectoryTool::new(project_path_buf.clone()))
1575+
.tool(WebFetchTool::new());
14711576

14721577
// Add generation tools if this is a generation query
14731578
if is_generation {
@@ -1515,7 +1620,8 @@ pub async fn run_query(
15151620
.tool(TerraformValidateTool::new(project_path_buf.clone()))
15161621
.tool(TerraformInstallTool::new())
15171622
.tool(ReadFileTool::new(project_path_buf.clone()))
1518-
.tool(ListDirectoryTool::new(project_path_buf.clone()));
1623+
.tool(ListDirectoryTool::new(project_path_buf.clone()))
1624+
.tool(WebFetchTool::new());
15191625

15201626
// Add generation tools if this is a generation query
15211627
if is_generation {

0 commit comments

Comments
 (0)