Skip to content

Commit ea1a405

Browse files
authored
Merge pull request #205 from syncable-dev/develop
Develop
2 parents 21b2697 + e1f2b93 commit ea1a405

6 files changed

Lines changed: 257 additions & 76 deletions

File tree

.DS_Store

6 KB
Binary file not shown.

src/agent/ide/client.rs

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ impl IdeClient {
126126
// Try to read connection config from file
127127
if let Some(config) = self.read_connection_config().await {
128128
self.port = Some(config.port);
129-
self.auth_token = config.auth_token;
129+
self.auth_token = config.auth_token.clone();
130130

131131
// Try to establish connection
132132
if self.establish_connection().await.is_ok() {
@@ -157,43 +157,31 @@ impl IdeClient {
157157
/// Read connection config from port file
158158
/// Supports both Syncable and Gemini CLI companion extensions
159159
async fn read_connection_config(&self) -> Option<ConnectionConfig> {
160-
let process_info = self.process_info.as_ref()?;
161-
let pid = process_info.pid;
162160
let temp_dir = env::temp_dir();
163161

164-
// Try Syncable extension first
162+
// Try Syncable extension first - scan all port files, match by workspace
165163
let syncable_port_dir = temp_dir.join("syncable").join("ide");
166-
if let Some(config) = self.find_port_file(&syncable_port_dir, "syncable-ide-server", pid) {
164+
if let Some(config) = self.find_port_file_by_workspace(&syncable_port_dir, "syncable-ide-server") {
167165
return Some(config);
168166
}
169167

170168
// Try Gemini CLI extension (for compatibility)
171169
let gemini_port_dir = temp_dir.join("gemini").join("ide");
172-
if let Some(config) = self.find_port_file(&gemini_port_dir, "gemini-ide-server", pid) {
170+
if let Some(config) = self.find_port_file_by_workspace(&gemini_port_dir, "gemini-ide-server") {
173171
return Some(config);
174172
}
175173

176-
// Legacy Gemini format (single file in temp dir)
177-
let legacy_gemini = temp_dir.join(format!("gemini-ide-server-{}.json", pid));
178-
if let Ok(content) = fs::read_to_string(&legacy_gemini) {
179-
if let Ok(config) = serde_json::from_str::<ConnectionConfig>(&content) {
180-
if self.validate_workspace_path(&config.workspace_path) {
181-
return Some(config);
182-
}
183-
}
184-
}
185-
186174
None
187175
}
188176

189-
/// Find a port file in a directory matching the given prefix and PID
190-
fn find_port_file(&self, dir: &PathBuf, prefix: &str, pid: u32) -> Option<ConnectionConfig> {
177+
/// Find a port file in a directory by scanning all files and matching workspace path
178+
fn find_port_file_by_workspace(&self, dir: &PathBuf, prefix: &str) -> Option<ConnectionConfig> {
191179
let entries = fs::read_dir(dir).ok()?;
192-
let file_prefix = format!("{}-{}-", prefix, pid);
193180

194181
for entry in entries.flatten() {
195182
let filename = entry.file_name().to_string_lossy().to_string();
196-
if filename.starts_with(&file_prefix) && filename.ends_with(".json") {
183+
// Match any file starting with the prefix and ending with .json
184+
if filename.starts_with(prefix) && filename.ends_with(".json") {
197185
if let Ok(content) = fs::read_to_string(entry.path()) {
198186
if let Ok(config) = serde_json::from_str::<ConnectionConfig>(&content) {
199187
if self.validate_workspace_path(&config.workspace_path) {
@@ -253,7 +241,10 @@ impl IdeClient {
253241
);
254242

255243
// Send initialize request
256-
let mut request = self.http_client.post(&url).json(&init_request);
244+
let mut request = self.http_client
245+
.post(&url)
246+
.header("Accept", "application/json, text/event-stream")
247+
.json(&init_request);
257248

258249
if let Some(token) = &self.auth_token {
259250
request = request.header("Authorization", format!("Bearer {}", token));
@@ -271,12 +262,15 @@ impl IdeClient {
271262
}
272263
}
273264

274-
// Parse response
275-
let response_data: JsonRpcResponse = response
276-
.json()
265+
// Parse response (SSE format: "event: message\ndata: {json}")
266+
let response_text = response
267+
.text()
277268
.await
278269
.map_err(|e| IdeError::ConnectionFailed(e.to_string()))?;
279270

271+
let response_data: JsonRpcResponse = Self::parse_sse_response(&response_text)
272+
.map_err(IdeError::ConnectionFailed)?;
273+
280274
if response_data.error.is_some() {
281275
return Err(IdeError::ConnectionFailed(
282276
response_data
@@ -289,6 +283,20 @@ impl IdeClient {
289283
Ok(())
290284
}
291285

286+
/// Parse SSE response format to extract JSON
287+
fn parse_sse_response(text: &str) -> Result<JsonRpcResponse, String> {
288+
// SSE format: "event: message\ndata: {json}\n\n"
289+
for line in text.lines() {
290+
if let Some(json_str) = line.strip_prefix("data: ") {
291+
return serde_json::from_str(json_str)
292+
.map_err(|e| format!("Failed to parse JSON: {}", e));
293+
}
294+
}
295+
// Fallback: try parsing entire response as JSON (for non-SSE responses)
296+
serde_json::from_str(text)
297+
.map_err(|e| format!("Failed to parse response: {}", e))
298+
}
299+
292300
/// Get next request ID
293301
fn next_request_id(&self) -> u64 {
294302
let mut id = self.request_id.lock().unwrap();
@@ -307,7 +315,10 @@ impl IdeClient {
307315

308316
let request = JsonRpcRequest::new(self.next_request_id(), method, params);
309317

310-
let mut http_request = self.http_client.post(&url).json(&request);
318+
let mut http_request = self.http_client
319+
.post(&url)
320+
.header("Accept", "application/json, text/event-stream")
321+
.json(&request);
311322

312323
if let Some(token) = &self.auth_token {
313324
http_request = http_request.header("Authorization", format!("Bearer {}", token));
@@ -322,10 +333,13 @@ impl IdeClient {
322333
.await
323334
.map_err(|e| IdeError::RequestFailed(e.to_string()))?;
324335

325-
response
326-
.json()
336+
let response_text = response
337+
.text()
327338
.await
328-
.map_err(|e| IdeError::RequestFailed(e.to_string()))
339+
.map_err(|e| IdeError::RequestFailed(e.to_string()))?;
340+
341+
Self::parse_sse_response(&response_text)
342+
.map_err(IdeError::RequestFailed)
329343
}
330344

331345
/// Open a diff view in the IDE

src/agent/mod.rs

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,18 @@ pub async fn run_interactive(
135135
);
136136
Some(Arc::new(TokioMutex::new(client)))
137137
}
138-
Err(_) => {
139-
// IDE detected but companion not running - that's fine
138+
Err(e) => {
139+
// IDE detected but companion not running or connection failed
140+
println!(
141+
"{} IDE companion not connected: {}",
142+
"!".yellow(),
143+
e
144+
);
140145
None
141146
}
142147
}
143148
} else {
149+
println!("{} No IDE detected (TERM_PROGRAM={})", "·".dimmed(), std::env::var("TERM_PROGRAM").unwrap_or_default());
144150
None
145151
}
146152
};
@@ -240,16 +246,23 @@ pub async fn run_interactive(
240246

241247
// Add generation tools if this is a generation query
242248
if is_generation {
243-
// Create WriteFileTool with IDE client if connected
244-
let write_file_tool = if let Some(ref client) = ide_client {
245-
WriteFileTool::new(project_path_buf.clone())
246-
.with_ide_client(client.clone())
249+
// Create file tools with IDE client if connected
250+
let (write_file_tool, write_files_tool) = if let Some(ref client) = ide_client {
251+
(
252+
WriteFileTool::new(project_path_buf.clone())
253+
.with_ide_client(client.clone()),
254+
WriteFilesTool::new(project_path_buf.clone())
255+
.with_ide_client(client.clone()),
256+
)
247257
} else {
248-
WriteFileTool::new(project_path_buf.clone())
258+
(
259+
WriteFileTool::new(project_path_buf.clone()),
260+
WriteFilesTool::new(project_path_buf.clone()),
261+
)
249262
};
250263
builder = builder
251264
.tool(write_file_tool)
252-
.tool(WriteFilesTool::new(project_path_buf.clone()))
265+
.tool(write_files_tool)
253266
.tool(ShellTool::new(project_path_buf.clone()));
254267
}
255268

@@ -281,16 +294,23 @@ pub async fn run_interactive(
281294

282295
// Add generation tools if this is a generation query
283296
if is_generation {
284-
// Create WriteFileTool with IDE client if connected
285-
let write_file_tool = if let Some(ref client) = ide_client {
286-
WriteFileTool::new(project_path_buf.clone())
287-
.with_ide_client(client.clone())
297+
// Create file tools with IDE client if connected
298+
let (write_file_tool, write_files_tool) = if let Some(ref client) = ide_client {
299+
(
300+
WriteFileTool::new(project_path_buf.clone())
301+
.with_ide_client(client.clone()),
302+
WriteFilesTool::new(project_path_buf.clone())
303+
.with_ide_client(client.clone()),
304+
)
288305
} else {
289-
WriteFileTool::new(project_path_buf.clone())
306+
(
307+
WriteFileTool::new(project_path_buf.clone()),
308+
WriteFilesTool::new(project_path_buf.clone()),
309+
)
290310
};
291311
builder = builder
292312
.tool(write_file_tool)
293-
.tool(WriteFilesTool::new(project_path_buf.clone()))
313+
.tool(write_files_tool)
294314
.tool(ShellTool::new(project_path_buf.clone()));
295315
}
296316

src/agent/tools/file_ops.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,8 @@ pub struct WriteFilesTool {
606606
require_confirmation: bool,
607607
/// Session-level allowed file patterns
608608
allowed_patterns: std::sync::Arc<AllowedFilePatterns>,
609+
/// Optional IDE client for native diff views
610+
ide_client: Option<std::sync::Arc<tokio::sync::Mutex<IdeClient>>>,
609611
}
610612

611613
impl WriteFilesTool {
@@ -614,6 +616,7 @@ impl WriteFilesTool {
614616
project_path,
615617
require_confirmation: true,
616618
allowed_patterns: std::sync::Arc::new(AllowedFilePatterns::new()),
619+
ide_client: None,
617620
}
618621
}
619622

@@ -626,6 +629,7 @@ impl WriteFilesTool {
626629
project_path,
627630
require_confirmation: true,
628631
allowed_patterns,
632+
ide_client: None,
629633
}
630634
}
631635

@@ -635,6 +639,12 @@ impl WriteFilesTool {
635639
self
636640
}
637641

642+
/// Set the IDE client for native diff views
643+
pub fn with_ide_client(mut self, ide_client: std::sync::Arc<tokio::sync::Mutex<IdeClient>>) -> Self {
644+
self.ide_client = Some(ide_client);
645+
self
646+
}
647+
638648
fn validate_path(&self, requested: &PathBuf) -> Result<PathBuf, WriteFilesError> {
639649
let canonical_project = self.project_path.canonicalize()
640650
.map_err(|e| WriteFilesError(format!("Invalid project path: {}", e)))?;
@@ -744,11 +754,31 @@ All files are written atomically - if any file fails, previously written files i
744754
&& !self.allowed_patterns.is_allowed(&filename);
745755

746756
if needs_confirmation {
747-
let confirmation = confirm_file_write(
748-
&file.path,
749-
old_content.as_deref(),
750-
&file.content,
751-
);
757+
// Use IDE diff if client is connected, otherwise terminal diff
758+
let confirmation = if let Some(ref client) = self.ide_client {
759+
let guard = client.lock().await;
760+
if guard.is_connected() {
761+
confirm_file_write_with_ide(
762+
&file.path,
763+
old_content.as_deref(),
764+
&file.content,
765+
Some(&*guard),
766+
).await
767+
} else {
768+
drop(guard);
769+
confirm_file_write(
770+
&file.path,
771+
old_content.as_deref(),
772+
&file.content,
773+
)
774+
}
775+
} else {
776+
confirm_file_write(
777+
&file.path,
778+
old_content.as_deref(),
779+
&file.content,
780+
)
781+
};
752782

753783
match confirmation {
754784
ConfirmationResult::Proceed => {

0 commit comments

Comments
 (0)