Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -574,3 +574,6 @@ data/
.graphify_detect.json
graphify-out/
.graphify_*

# sentry-patrol per-issue MCP server proposal configs (local-only, contain absolute paths)
mcp-workflow-proposal-ELECTRON-*.json
38 changes: 38 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ members = [
"crates/aionui-auth",
"crates/aionui-system",
"crates/aionui-file",
"crates/aionui-static-file",
"crates/aionui-office",
"crates/aionui-shell",
"crates/aionui-ai-agent",
Expand Down Expand Up @@ -39,6 +40,7 @@ aionui-runtime = { path = "crates/aionui-runtime" }
aionui-auth = { path = "crates/aionui-auth" }
aionui-system = { path = "crates/aionui-system" }
aionui-file = { path = "crates/aionui-file" }
aionui-static-file = { path = "crates/aionui-static-file" }
aionui-office = { path = "crates/aionui-office" }
aionui-shell = { path = "crates/aionui-shell" }
aionui-ai-agent = { path = "crates/aionui-ai-agent" }
Expand Down Expand Up @@ -119,9 +121,13 @@ dirs = "6"
# Regex
regex = "1"

# Async stream utilities
tokio-util = { version = "0.7", features = ["io"] }

# File system
include_dir = "0.7"
notify = "8"
notify-debouncer-full = "0.7"
walkdir = "2"
ignore = "0.4"
zip = "2"
Expand Down
8 changes: 2 additions & 6 deletions crates/aionui-api-types/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ pub struct ReadFileBufferRequest {
pub struct WriteFileRequest {
pub path: String,
pub data: String,
/// Workspace root, used to compute `relativePath` in the
/// `fileStream.contentUpdate` event. Falls back to the file's
/// parent directory when absent.
/// Workspace root. Falls back to the file's parent directory when absent.
#[serde(default)]
pub workspace: Option<String>,
}
Expand All @@ -67,9 +65,7 @@ pub struct CopyFilesRequest {
#[derive(Debug, Deserialize)]
pub struct RemoveEntryRequest {
pub path: String,
/// Workspace root, used to compute `relativePath` in the
/// `fileStream.contentUpdate` event. Falls back to the file's
/// parent directory when absent.
/// Workspace root. Falls back to the file's parent directory when absent.
#[serde(default)]
pub workspace: Option<String>,
}
Expand Down
2 changes: 2 additions & 0 deletions crates/aionui-api-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod system;
mod team;
mod team_mcp;
mod websocket;
mod workspace_watcher;

pub use acp::{
AcpEnvResponse, AcpHealthCheckRequest, AcpHealthCheckResponse, AgentModeResponse, DetectCliRequest,
Expand Down Expand Up @@ -133,3 +134,4 @@ pub use team::{
};
pub use team_mcp::{GuideMcpConfig, TeamMcpStdioConfig};
pub use websocket::WebSocketMessage;
pub use workspace_watcher::{WatchBatchEvent, WatchChange, WatchChangeKind, WatchOverflowEvent};
35 changes: 35 additions & 0 deletions crates/aionui-api-types/src/workspace_watcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! WebSocket event types for the workspace file watcher.
//!
//! Emitted by `aionui-file::workspace_watcher` and consumed by the frontend
//! via `WebSocketMessage<T>`.

use serde::{Deserialize, Serialize};

/// Kind of file-system change detected.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum WatchChangeKind {
Create,
Modify,
Delete,
}

/// A single file-system change within a workspace.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchChange {
pub path: String,
pub kind: WatchChangeKind,
}

/// Batch event pushed to subscribed connections.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchBatchEvent {
pub workspace: String,
pub changes: Vec<WatchChange>,
}

/// Overflow event when too many changes occur in a single batch.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchOverflowEvent {
pub workspace: String,
}
1 change: 1 addition & 0 deletions crates/aionui-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ aionui-shell.workspace = true
aionui-ai-agent.workspace = true
aionui-mcp.workspace = true
aionui-conversation.workspace = true
aionui-static-file.workspace = true
aionui-extension.workspace = true
aionui-channel.workspace = true
aionui-team.workspace = true
Expand Down
7 changes: 6 additions & 1 deletion crates/aionui-app/src/router/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use aionui_auth::{
use aionui_channel::channel_routes;
#[cfg(feature = "weixin")]
use aionui_channel::weixin_login_route;
use aionui_conversation::{conversation_ops_routes, conversation_routes};
use aionui_conversation::{conversation_ops_routes, conversation_routes, conversation_static_file_routes};
use aionui_cron::cron_routes;
use aionui_extension::{extension_routes, hub_routes, skill_routes};
use aionui_file::file_routes;
Expand Down Expand Up @@ -177,6 +177,10 @@ pub fn create_router_with_all_state(services: &AppServices, states: ModuleStates
let assistant_authenticated =
assistant_routes(states.assistant).route_layer(from_fn_with_state(auth_mw_state.clone(), auth_middleware));

// Static file serving for conversation workspaces, protected by auth
let static_file_authenticated = conversation_static_file_routes(states.static_file)
.route_layer(from_fn_with_state(auth_mw_state.clone(), auth_middleware));

// Guide MCP diagnostic endpoint protected by auth middleware
let guide_mcp_authenticated = Router::new()
.route("/api/system/guide-mcp", get(guide_mcp_status))
Expand Down Expand Up @@ -211,6 +215,7 @@ pub fn create_router_with_all_state(services: &AppServices, states: ModuleStates
.merge(office_authenticated)
.merge(shell_authenticated)
.merge(assistant_authenticated)
.merge(static_file_authenticated)
.merge(guide_mcp_authenticated);

// Conditionally merge WeChat login SSE route (feature-gated)
Expand Down
38 changes: 31 additions & 7 deletions crates/aionui-app/src/router/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use aionui_ai_agent::{AgentRouterState, AgentService, RemoteAgentRouterState, Re
use aionui_assistant::{AssistantRouterState, AssistantService, BuiltinAssistantRegistry};
use aionui_auth::extract_token_from_ws_headers;
use aionui_channel::ChannelRouterState;
use aionui_conversation::{ConversationRouterState, ConversationService};
use aionui_conversation::{ConversationRouterState, ConversationService, StaticFileRouterState};
use aionui_cron::{CronEventEmitter, CronRouterState};
use aionui_db::{
IAcpSessionRepository, IAgentMetadataRepository, IAssistantOverrideRepository, IAssistantRepository,
Expand All @@ -22,7 +22,10 @@ use aionui_extension::{
HubIndexManager, HubInstaller, HubRouterState, SkillRouterState, resolve_install_target_dir_for_data_dir,
resolve_scan_paths_for_data_dir, resolve_state_file_path,
};
use aionui_file::{FileRouterState, FileService, FileWatchService, SnapshotService};
use aionui_file::{
EventDispatcher, FileRouterState, FileService, FileWatchService, GitignoreFilter, SnapshotService,
SubscriptionRegistry, WorkspaceWatchManager, WorkspaceWatchRouter,
};
use aionui_mcp::{
AionrsAdapter, AionuiAdapter, ClaudeAdapter, CodeBuddyAdapter, CodexAdapter, GeminiAdapter, McpAgentAdapter,
McpConfigService, McpConnectionTestService, McpRouterState, McpSyncService, OpencodeAdapter, QwenAdapter,
Expand All @@ -31,8 +34,9 @@ use aionui_office::{
ConversionService, OfficeRouterState, OfficecliWatchManager, ProxyService,
SnapshotService as OfficeSnapshotService, StarOfficeDetector,
};
use aionui_realtime::{NoopMessageRouter, WsHandlerState};
use aionui_realtime::WsHandlerState;
use aionui_shell::ShellRouterState;
use aionui_static_file::StaticFileService;
use aionui_system::{
ClientPrefService, ConnectionTestRouterState, ConnectionTestService, ModelFetchService, ProtocolDetectionService,
ProviderService, SettingsService, SystemRouterState, VersionCheckService,
Expand Down Expand Up @@ -63,6 +67,7 @@ pub struct ModuleStates {
pub cron: CronRouterState,
pub office: OfficeRouterState,
pub shell: ShellRouterState,
pub static_file: StaticFileRouterState,
pub assistant: AssistantRouterState,
}

Expand Down Expand Up @@ -130,9 +135,15 @@ pub async fn build_module_states(services: &AppServices) -> (ModuleStates, Chann

let agent_service = AgentService::new(services.agent_registry.clone(), services.data_dir.clone());

let conversation_state = build_conversation_state(services, Some(cron.cron_service.clone()));
let static_file = StaticFileRouterState {
conversation_service: conversation_state.service.clone(),
static_file_service: Arc::new(StaticFileService::permissive()),
};

let states = ModuleStates {
system: build_system_state(services),
conversation: build_conversation_state(services, Some(cron.cron_service.clone())),
conversation: conversation_state,
remote_agent: build_remote_agent_state(services),
agent: AgentRouterState {
agent_registry: services.agent_registry.clone(),
Expand All @@ -155,6 +166,7 @@ pub async fn build_module_states(services: &AppServices) -> (ModuleStates, Chann
office: build_office_state(services),
shell: build_shell_state(services),
assistant,
static_file,
};

(states, channel_components)
Expand Down Expand Up @@ -255,7 +267,7 @@ pub fn build_file_state(services: &AppServices) -> FileRouterState {
let broadcaster = services.event_bus.clone();
let allowed_roots = default_allowed_roots(Some(services.work_dir.as_path()));
let browse_roots = aionui_file::browse::default_browse_roots();
let file_service = Arc::new(FileService::new(broadcaster.clone(), allowed_roots.clone()));
let file_service = Arc::new(FileService::new(allowed_roots.clone()));
let watch_service = Arc::new(FileWatchService::new(broadcaster).expect("file watch service initialization"));
let snapshot_service = Arc::new(SnapshotService::new());
FileRouterState {
Expand Down Expand Up @@ -610,10 +622,22 @@ pub async fn build_extension_states(

/// Build the default `WsHandlerState` from application services.
pub fn build_ws_state(services: &AppServices) -> WsHandlerState {
let registry = Arc::new(SubscriptionRegistry::new());
let gitignore = Arc::new(GitignoreFilter::new());

let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
let watch_manager: Arc<WorkspaceWatchManager> = Arc::new(WorkspaceWatchManager::new(event_tx));
let router = Arc::new(WorkspaceWatchRouter::new(registry.clone(), watch_manager));

// Spawn the event dispatch loop (debouncer-full handles aggregation)
let dispatcher = EventDispatcher::new(registry, services.ws_manager.clone(), gitignore)
.with_office_broadcaster(services.event_bus.clone());
tokio::spawn(dispatcher.run(event_rx));

if services.local {
return WsHandlerState {
manager: services.ws_manager.clone(),
router: Arc::new(NoopMessageRouter),
router,
token_validator: Arc::new(|_| true),
token_extractor: Arc::new(|_| Some("local".into())),
};
Expand All @@ -626,7 +650,7 @@ pub fn build_ws_state(services: &AppServices) -> WsHandlerState {

WsHandlerState {
manager: services.ws_manager.clone(),
router: Arc::new(NoopMessageRouter),
router,
token_validator,
token_extractor,
}
Expand Down
2 changes: 1 addition & 1 deletion crates/aionui-app/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ pub async fn build_app_with_file_roots(allowed_roots: Vec<std::path::PathBuf>) -
let db = aionui_db::init_database_memory().await.unwrap();
let services = AppServices::from_config(db, &AppConfig::default()).await.unwrap();
let (mut states, _) = build_module_states(&services).await;
states.file.file_service = std::sync::Arc::new(FileService::new(services.event_bus.clone(), allowed_roots));
states.file.file_service = std::sync::Arc::new(FileService::new(allowed_roots));
let router = create_router_with_states(&services, states);
(router, services)
}
Expand Down
Loading
Loading