Skip to content

Commit bec3502

Browse files
committed
Refine ACP and watcher handling
1 parent 38b7830 commit bec3502

13 files changed

Lines changed: 387 additions & 87 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ anycode/dist
77
anycode-base/dist
88
anycode-backend/dist/*
99
!anycode-backend/dist/.gitkeep
10+
csharp-test-project

README.md

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# anycode
22

3-
**anycode** is a lightning-fast web-based IDE that allows you to write, edit, and manage code directly from your browser. Built for speed and performance, anycode supports a wide range of programming languages and provides an intuitive interface with powerful features for a seamless development experience.
3+
**anycode** is a web platform where people and agents build together. Run it locally or remotely and work from any device you want: desktop, laptop, mobile, or even VR. Use any agent, including Codex, Claude Code, OpenCode, as well as local models.
44

55
![editor](anycode/imgs/screen.png)
66
![agents](anycode/imgs/agents.png)
@@ -60,35 +60,6 @@ One-shot installer:
6060
curl -fsSL https://raw.githubusercontent.com/anycode-ade/anycode/main/install.sh | sh
6161
```
6262

63-
You can also pin a version:
64-
```bash
65-
curl -fsSL https://raw.githubusercontent.com/anycode-ade/anycode/main/install.sh | sh -s -- --version v0.0.10
66-
```
67-
68-
Linux (x86_64):
69-
```bash
70-
curl -L https://github.com/anycode-ade/anycode/releases/latest/download/anycode-linux-x86_64-musl.tar.gz | tar -xz
71-
sudo mv anycode /usr/local/bin/
72-
sudo chmod +x /usr/local/bin/anycode
73-
anycode
74-
```
75-
76-
Linux (ARM64):
77-
```bash
78-
curl -L https://github.com/anycode-ade/anycode/releases/latest/download/anycode-linux-aarch64-musl.tar.gz | tar -xz
79-
sudo mv anycode /usr/local/bin/
80-
sudo chmod +x /usr/local/bin/anycode
81-
anycode
82-
```
83-
84-
MacOS:
85-
```bash
86-
curl -L https://github.com/anycode-ade/anycode/releases/latest/download/anycode-universal-apple-darwin.tar.gz | tar -xz
87-
sudo mv anycode /usr/local/bin/
88-
sudo chmod +x /usr/local/bin/anycode
89-
anycode
90-
```
91-
9263
## Development
9364

9465
1. **Start frontend:**

anycode-backend/src/acp.rs

Lines changed: 42 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::sync::Arc;
1111
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
1212
use tokio::io::{self, AsyncBufReadExt, BufReader};
1313
use tokio::process::Command;
14-
use tokio::sync::{Mutex, RwLock, broadcast, mpsc, oneshot::Sender};
14+
use tokio::sync::{Mutex, RwLock, broadcast, mpsc, oneshot};
1515
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
1616
use tracing::{debug, error, info};
1717

@@ -229,7 +229,7 @@ struct AcpClientImpl {
229229
message_sender: broadcast::Sender<AcpMessage>,
230230
history: Arc<tokio::sync::Mutex<Vec<AcpMessage>>>,
231231
/// Pending permission requests waiting for user response
232-
pending_permissions: Arc<Mutex<HashMap<String, Sender<PermissionResponse>>>>,
232+
pending_permissions: Arc<Mutex<HashMap<String, oneshot::Sender<PermissionResponse>>>>,
233233
/// Channel to send file operations to the ACP filesystem background task
234234
fs_sender: Option<mpsc::Sender<AcpFsCommand>>,
235235
}
@@ -1099,7 +1099,7 @@ impl AcpClientImpl {
10991099

11001100
/// Type alias for pending permissions map
11011101
pub type PendingPermissionsMap =
1102-
Arc<tokio::sync::Mutex<HashMap<String, tokio::sync::oneshot::Sender<PermissionResponse>>>>;
1102+
Arc<tokio::sync::Mutex<HashMap<String, oneshot::Sender<PermissionResponse>>>>;
11031103

11041104
enum RestoreSessionOutcome {
11051105
Restored(SessionBootstrap),
@@ -1204,7 +1204,7 @@ impl AcpAgent {
12041204
*cancel_sender_guard = Some(cancel_tx.clone());
12051205
}
12061206

1207-
let (session_tx, mut session_rx) = mpsc::channel::<acp::SessionId>(1);
1207+
let (bootstrap_tx, bootstrap_rx) = oneshot::channel::<Result<SessionBootstrap>>();
12081208

12091209
// Spawn agent process
12101210
let (mut child, stdin, stdout, stderr) = Self::spawn_agent_process(cmd, args)?;
@@ -1237,7 +1237,7 @@ impl AcpAgent {
12371237
prompt_rx,
12381238
config_rx,
12391239
cancel_rx,
1240-
session_tx,
1240+
bootstrap_tx,
12411241
resume_session_id,
12421242
)
12431243
.await;
@@ -1255,15 +1255,19 @@ impl AcpAgent {
12551255
});
12561256
self.process_handle = Some(process_handle);
12571257

1258-
match tokio::time::timeout(tokio::time::Duration::from_secs(15), session_rx.recv()).await {
1259-
Ok(Some(session_id)) => {
1260-
self.session_id = Some(session_id.clone());
1261-
Ok(session_id.to_string())
1258+
match tokio::time::timeout(tokio::time::Duration::from_secs(15), bootstrap_rx).await {
1259+
Ok(Ok(Ok(bootstrap))) => {
1260+
self.session_id = Some(bootstrap.session_id.clone());
1261+
Ok(bootstrap.session_id.to_string())
12621262
}
1263-
Ok(None) => {
1263+
Ok(Ok(Err(e))) => {
1264+
self.stop().await;
1265+
Err(e)
1266+
}
1267+
Ok(Err(_)) => {
12641268
self.stop().await;
12651269
Err(anyhow!(
1266-
"ACP agent session channel closed before initialization"
1270+
"ACP agent initialization channel closed before initialization"
12671271
))
12681272
}
12691273
Err(_) => {
@@ -1313,7 +1317,7 @@ impl AcpAgent {
13131317
mut prompt_rx: mpsc::Receiver<String>,
13141318
mut config_rx: mpsc::Receiver<PendingConfigUpdate>,
13151319
mut cancel_rx: mpsc::Receiver<()>,
1316-
session_tx: mpsc::Sender<acp::SessionId>,
1320+
bootstrap_tx: oneshot::Sender<Result<SessionBootstrap>>,
13171321
resume_session_id: Option<String>,
13181322
) {
13191323
// Clone history before moving client_impl
@@ -1353,47 +1357,41 @@ impl AcpAgent {
13531357
let bootstrap = match Self::initialize_connection(&conn, &agent_id, resume_session_id).await
13541358
{
13551359
Ok(bootstrap) => {
1356-
let _ = session_tx.send(bootstrap.session_id.clone()).await;
13571360
ready.store(true, Ordering::SeqCst);
1361+
let _ = bootstrap_tx.send(Ok(bootstrap.clone()));
13581362
Some(bootstrap)
13591363
}
13601364
Err(e) => {
1361-
error!("Failed to initialize ACP agent {}: {}", agent_id, e);
1365+
let err = anyhow!("Failed to initialize ACP agent {}: {}", agent_id, e);
1366+
error!("{}", err);
1367+
let _ = bootstrap_tx.send(Err(err));
13621368
None
13631369
}
13641370
};
13651371

1366-
// Handle prompts in a loop
1367-
if let Some(bootstrap) = bootstrap {
1368-
Self::emit_session_config_messages(
1369-
&message_sender,
1370-
&history_for_prompt,
1371-
bootstrap.model_selector.clone(),
1372-
bootstrap.reasoning_selector.clone(),
1373-
)
1374-
.await;
1372+
let Some(bootstrap) = bootstrap else {
1373+
return;
1374+
};
13751375

1376-
Self::run_prompt_loop(
1377-
&conn,
1378-
&agent_id,
1379-
&message_sender,
1380-
history_for_prompt,
1381-
bootstrap.session_id,
1382-
&mut prompt_rx,
1383-
&mut config_rx,
1384-
&mut cancel_rx,
1385-
)
1386-
.await;
1387-
} else {
1388-
error!(
1389-
"No session ID for agent {}, cannot handle prompts",
1390-
agent_id
1391-
);
1392-
// Keep the connection alive if no session
1393-
loop {
1394-
tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
1395-
}
1396-
}
1376+
Self::emit_session_config_messages(
1377+
&message_sender,
1378+
&history_for_prompt,
1379+
bootstrap.model_selector.clone(),
1380+
bootstrap.reasoning_selector.clone(),
1381+
)
1382+
.await;
1383+
1384+
Self::run_prompt_loop(
1385+
&conn,
1386+
&agent_id,
1387+
&message_sender,
1388+
history_for_prompt,
1389+
bootstrap.session_id,
1390+
&mut prompt_rx,
1391+
&mut config_rx,
1392+
&mut cancel_rx,
1393+
)
1394+
.await;
13971395
}
13981396

13991397
fn spawn_stderr_reader(

anycode-backend/src/handlers/acp_handler.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ pub async fn handle_acp_start(
7878
}
7979
Err(e) => {
8080
error!("Failed to start ACP agent {}: {}", agent_id, e);
81+
let _ = socket.emit(
82+
"acp:message",
83+
&json!({
84+
"agent_id": agent_id,
85+
"item": {
86+
"role": "error",
87+
"message": format!("Failed to start agent: {}", e),
88+
}
89+
}),
90+
);
8191
error_ack!(ack, &agent_id, "Failed to start agent: {}", e);
8292
}
8393
}

anycode-backend/src/handlers/watch_handler.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::code::Code;
1414
use crate::diff::compute_text_edits;
1515
use crate::handlers::io_handler::apply_edits_to_code;
1616
use crate::lsp::LspManager;
17+
use crate::utils::normalize_watch_path;
1718

1819
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1920
enum FileState {
@@ -147,10 +148,8 @@ pub async fn handle_watch_event(
147148
git_manager: &Arc<Mutex<crate::git::GitManager>>,
148149
lsp_manager: &Arc<Mutex<LspManager>>,
149150
) {
150-
let path_str = match path.to_str() {
151-
Some(s) => s.to_string(),
152-
None => return,
153-
};
151+
let normalized_path = normalize_watch_path(path);
152+
let path_str = normalized_path.to_string_lossy().to_string();
154153

155154
let (should_spawn, rx) = {
156155
let mut states = file_states.lock().await;
@@ -181,7 +180,7 @@ pub async fn handle_watch_event(
181180
let mut rx = rx.unwrap();
182181

183182
// Spawn a single debounce task for this file
184-
let path = path.clone();
183+
let path = normalized_path.clone();
185184
let socket = socket.clone();
186185
let file2code = file2code.clone();
187186
let socket2data = socket2data.clone();
@@ -384,6 +383,8 @@ async fn handle_file_modification(
384383
#[cfg(test)]
385384
mod tests {
386385
use super::*;
386+
use crate::utils::current_dir;
387+
use std::path::Path;
387388

388389
#[test]
389390
fn unopened_new_file_is_classified_as_create() {
@@ -408,4 +409,14 @@ mod tests {
408409
WatchAction::Modify
409410
);
410411
}
412+
413+
#[test]
414+
fn watcher_paths_are_normalized_before_comparison() {
415+
let cwd = current_dir();
416+
let normalized = normalize_watch_path(Path::new("./test.js"));
417+
assert_eq!(normalized, cwd.join("test.js"));
418+
419+
let normalized = normalize_watch_path(Path::new(&cwd.join("./test.js")));
420+
assert_eq!(normalized, cwd.join("test.js"));
421+
}
411422
}

anycode-backend/src/search.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,6 @@ pub async fn global_search(
270270
}
271271

272272
pub mod search_exp {
273-
274273

275274
#[test]
276275
fn test_line_search_simple() {

anycode-backend/src/utils.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use lsp_types::Uri;
22
use pathdiff::diff_paths;
3-
use std::path::{Path, PathBuf};
3+
use std::path::{Component, Path, PathBuf};
44

55
pub const DEFAULT_IGNORE_DIRS: &[&str] = &[
66
// Version control and IDEs
@@ -182,6 +182,34 @@ pub fn abs_file(input: &str) -> anyhow::Result<String> {
182182
Ok(c.to_string_lossy().to_string())
183183
}
184184

185+
/// Normalize a path for watcher comparisons without requiring the file to exist.
186+
///
187+
/// This resolves `.` and `..` segments and makes relative paths absolute against the
188+
/// current working directory, but does not follow symlinks.
189+
pub fn normalize_watch_path(path: &Path) -> PathBuf {
190+
let absolute = if path.is_absolute() {
191+
path.to_path_buf()
192+
} else {
193+
current_dir().join(path)
194+
};
195+
196+
let mut normalized = PathBuf::new();
197+
198+
for component in absolute.components() {
199+
match component {
200+
Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
201+
Component::RootDir => normalized.push(component.as_os_str()),
202+
Component::CurDir => {}
203+
Component::ParentDir => {
204+
normalized.pop();
205+
}
206+
Component::Normal(part) => normalized.push(part),
207+
}
208+
}
209+
210+
normalized
211+
}
212+
185213
pub fn file_name(input: &str) -> String {
186214
let path_buf = std::path::PathBuf::from(input);
187215
let file_name = path_buf.file_name().unwrap().to_string_lossy().into_owned();

anycode/components/agent/AcpInput.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@
6464

6565
.acp-input-preview-text {
6666
flex: 1;
67-
font-weight: 500;
68-
font-size: 14px;
69-
line-height: 1.5;
67+
68+
font-size: 12px;
69+
7070
color: rgba(255, 255, 255, 0.7);
7171
white-space: nowrap;
7272
overflow: hidden;

anycode/components/agent/AcpMessage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const SUPPORTED_LANGUAGES: Record<string, string> = {
5555
cxx: 'cpp',
5656
hpp: 'cpp',
5757
h: 'c',
58-
md: 'text',
58+
md: 'markdown',
5959
markdown: 'text',
6060
diff: 'text',
6161
text: 'text',

anycode/hooks/useAgents.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,8 @@ export const useAgents = ({
427427
setSelectedAgentId(aid);
428428
onAgentStarted?.();
429429
} else {
430-
alert('Failed to start agent ' + aid + ': ' + response.error);
430+
const errorMessage = response.error || `Failed to start agent ${aid}`;
431+
alert(errorMessage);
431432
}
432433
});
433434
return aid;

0 commit comments

Comments
 (0)