From 30913b2cc4a0df7a5685b570f2f5d33f19a543af Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 10 Apr 2026 09:40:01 -0700 Subject: [PATCH] feat: add ecc2 computer use remote dispatch --- ecc2/src/config/mod.rs | 93 ++++++++++++- ecc2/src/main.rs | 268 +++++++++++++++++++++++++++++++++++- ecc2/src/session/manager.rs | 199 +++++++++++++++++++++++++- ecc2/src/session/mod.rs | 27 ++++ ecc2/src/session/store.rs | 78 +++++++---- ecc2/src/tui/dashboard.rs | 1 + 6 files changed, 635 insertions(+), 31 deletions(-) diff --git a/ecc2/src/config/mod.rs b/ecc2/src/config/mod.rs index 0f083e39..d48bd9a6 100644 --- a/ecc2/src/config/mod.rs +++ b/ecc2/src/config/mod.rs @@ -50,6 +50,16 @@ pub struct ConflictResolutionConfig { pub notify_lead: bool, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct ComputerUseDispatchConfig { + pub agent: Option, + pub profile: Option, + pub use_worktree: bool, + pub project: Option, + pub task_group: Option, +} + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct AgentProfileConfig { @@ -223,6 +233,7 @@ pub struct Config { pub agent_profiles: BTreeMap, pub orchestration_templates: BTreeMap, pub memory_connectors: BTreeMap, + pub computer_use_dispatch: ComputerUseDispatchConfig, pub auto_dispatch_unread_handoffs: bool, pub auto_dispatch_limit_per_session: usize, pub auto_create_worktrees: bool, @@ -289,6 +300,7 @@ impl Default for Config { agent_profiles: BTreeMap::new(), orchestration_templates: BTreeMap::new(), memory_connectors: BTreeMap::new(), + computer_use_dispatch: ComputerUseDispatchConfig::default(), auto_dispatch_unread_handoffs: false, auto_dispatch_limit_per_session: 5, auto_create_worktrees: true, @@ -347,6 +359,26 @@ impl Config { self.budget_alert_thresholds.sanitized() } + pub fn computer_use_dispatch_defaults(&self) -> ResolvedComputerUseDispatchConfig { + let agent = self + .computer_use_dispatch + .agent + .clone() + .unwrap_or_else(|| self.default_agent.clone()); + let profile = self + .computer_use_dispatch + .profile + .clone() + .or_else(|| self.default_agent_profile.clone()); + ResolvedComputerUseDispatchConfig { + agent, + profile, + use_worktree: self.computer_use_dispatch.use_worktree, + project: self.computer_use_dispatch.project.clone(), + task_group: self.computer_use_dispatch.task_group.clone(), + } + } + pub fn resolve_agent_profile(&self, name: &str) -> Result { let mut chain = Vec::new(); self.resolve_agent_profile_inner(name, &mut chain) @@ -771,6 +803,27 @@ impl Default for HarnessRunnerConfig { } } +impl Default for ComputerUseDispatchConfig { + fn default() -> Self { + Self { + agent: None, + profile: None, + use_worktree: false, + project: None, + task_group: None, + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ResolvedComputerUseDispatchConfig { + pub agent: String, + pub profile: Option, + pub use_worktree: bool, + pub project: Option, + pub task_group: Option, +} + fn merge_unique(base: &mut Vec, additions: &[T]) where T: Clone + PartialEq, @@ -851,8 +904,8 @@ impl BudgetAlertThresholds { #[cfg(test)] mod tests { use super::{ - BudgetAlertThresholds, Config, ConflictResolutionConfig, ConflictResolutionStrategy, - PaneLayout, + BudgetAlertThresholds, ComputerUseDispatchConfig, Config, ConflictResolutionConfig, + ConflictResolutionStrategy, PaneLayout, ResolvedComputerUseDispatchConfig, }; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use std::collections::BTreeMap; @@ -1202,6 +1255,42 @@ notify_lead = false ); } + #[test] + fn computer_use_dispatch_deserializes_from_toml() { + let config: Config = toml::from_str( + r#" +[computer_use_dispatch] +agent = "codex" +profile = "browser" +use_worktree = true +project = "ops" +task_group = "remote browser" +"#, + ) + .unwrap(); + + assert_eq!( + config.computer_use_dispatch, + ComputerUseDispatchConfig { + agent: Some("codex".to_string()), + profile: Some("browser".to_string()), + use_worktree: true, + project: Some("ops".to_string()), + task_group: Some("remote browser".to_string()), + } + ); + assert_eq!( + config.computer_use_dispatch_defaults(), + ResolvedComputerUseDispatchConfig { + agent: "codex".to_string(), + profile: Some("browser".to_string()), + use_worktree: true, + project: Some("ops".to_string()), + task_group: Some("remote browser".to_string()), + } + ); + } + #[test] fn agent_profiles_resolve_inheritance_and_defaults() { let config: Config = toml::from_str( diff --git a/ecc2/src/main.rs b/ecc2/src/main.rs index 33580245..78e4bf46 100644 --- a/ecc2/src/main.rs +++ b/ecc2/src/main.rs @@ -45,6 +45,28 @@ impl WorktreePolicyArgs { } } +#[derive(clap::Args, Debug, Clone, Default)] +struct OptionalWorktreePolicyArgs { + /// Create a dedicated worktree + #[arg(short = 'w', long = "worktree", action = clap::ArgAction::SetTrue, overrides_with = "no_worktree")] + worktree: bool, + /// Skip dedicated worktree creation + #[arg(long = "no-worktree", action = clap::ArgAction::SetTrue, overrides_with = "worktree")] + no_worktree: bool, +} + +impl OptionalWorktreePolicyArgs { + fn resolve(&self, default_value: bool) -> bool { + if self.worktree { + true + } else if self.no_worktree { + false + } else { + default_value + } + } +} + #[derive(clap::Subcommand, Debug)] enum Commands { /// Launch the TUI dashboard @@ -479,6 +501,41 @@ enum RemoteCommands { #[arg(long)] json: bool, }, + /// Queue a remote computer-use task request + ComputerUse { + /// Goal to complete with computer-use/browser tools + #[arg(long)] + goal: String, + /// Optional target URL to open first + #[arg(long)] + target_url: Option, + /// Extra context for the operator + #[arg(long)] + context: Option, + /// Optional lead session ID or alias to route through + #[arg(long)] + to_session: Option, + /// Task priority + #[arg(long, value_enum, default_value_t = TaskPriorityArg::Normal)] + priority: TaskPriorityArg, + /// Agent type override (defaults to [computer_use_dispatch] or ECC default agent) + #[arg(short, long)] + agent: Option, + /// Agent profile override (defaults to [computer_use_dispatch] or ECC default profile) + #[arg(long)] + profile: Option, + #[command(flatten)] + worktree: OptionalWorktreePolicyArgs, + /// Optional project grouping override + #[arg(long)] + project: Option, + /// Optional task-group grouping override + #[arg(long)] + task_group: Option, + /// Emit machine-readable JSON instead of the human summary + #[arg(long)] + json: bool, + }, /// List queued remote task requests List { /// Include already dispatched or failed requests @@ -816,6 +873,20 @@ struct RemoteDispatchHttpRequest { task_group: Option, } +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +struct RemoteComputerUseHttpRequest { + goal: String, + target_url: Option, + context: Option, + to_session: Option, + priority: Option, + agent: Option, + profile: Option, + use_worktree: Option, + project: Option, + task_group: Option, +} + #[derive(Debug, Clone, Default, Deserialize)] #[serde(default)] struct JsonlMemoryConnectorRecord { @@ -1996,6 +2067,57 @@ async fn main() -> Result<()> { } } } + RemoteCommands::ComputerUse { + goal, + target_url, + context, + to_session, + priority, + agent, + profile, + worktree, + project, + task_group, + json, + } => { + let target_session_id = to_session + .as_deref() + .map(|value| resolve_session_id(&db, value)) + .transpose()?; + let defaults = cfg.computer_use_dispatch_defaults(); + let request = session::manager::create_computer_use_remote_dispatch_request( + &db, + &cfg, + &goal, + target_url.as_deref(), + context.as_deref(), + target_session_id.as_deref(), + priority.into(), + agent.as_deref(), + profile.as_deref(), + Some(worktree.resolve(defaults.use_worktree)), + session::SessionGrouping { + project, + task_group, + }, + "cli_computer_use", + None, + )?; + if json { + println!("{}", serde_json::to_string_pretty(&request)?); + } else { + println!( + "Queued remote {} request #{} [{}] {}", + request.request_kind, request.id, request.priority, goal + ); + if let Some(target_url) = request.target_url.as_deref() { + println!("- target url {target_url}"); + } + if let Some(target_session_id) = request.target_session_id.as_deref() { + println!("- target {}", short_session(target_session_id)); + } + } + } RemoteCommands::List { all, limit, json } => { let requests = session::manager::list_remote_dispatch_requests(&db, all, limit)?; if json { @@ -2010,9 +2132,15 @@ async fn main() -> Result<()> { .as_deref() .map(short_session) .unwrap_or_else(|| "new-session".to_string()); + let label = format_remote_dispatch_kind(request.request_kind); println!( - "#{} [{}] {} -> {} | {}", - request.id, request.priority, request.status, target, request.task + "#{} [{}] {} {} -> {} | {}", + request.id, + request.priority, + label, + request.status, + target, + request.task.lines().next().unwrap_or(&request.task) ); } } @@ -3096,6 +3224,13 @@ fn format_remote_dispatch_action(action: &session::manager::RemoteDispatchAction } } +fn format_remote_dispatch_kind(kind: session::RemoteDispatchKind) -> &'static str { + match kind { + session::RemoteDispatchKind::Standard => "standard", + session::RemoteDispatchKind::ComputerUse => "computer_use", + } +} + fn short_session(session_id: &str) -> String { session_id.chars().take(8).collect() } @@ -3225,6 +3360,86 @@ fn handle_remote_dispatch_connection( &serde_json::to_string(&request)?, ) } + ("POST", "/computer-use") => { + let auth = headers + .get("authorization") + .map(String::as_str) + .unwrap_or_default(); + let expected = format!("Bearer {bearer_token}"); + if auth != expected { + return write_http_response( + stream, + 401, + "application/json", + &serde_json::json!({"error": "unauthorized"}).to_string(), + ); + } + + let payload: RemoteComputerUseHttpRequest = + serde_json::from_slice(&body).context("Invalid remote computer-use JSON body")?; + if payload.goal.trim().is_empty() { + return write_http_response( + stream, + 400, + "application/json", + &serde_json::json!({"error": "goal is required"}).to_string(), + ); + } + + let target_session_id = match payload + .to_session + .as_deref() + .map(|value| resolve_session_id(db, value)) + .transpose() + { + Ok(value) => value, + Err(error) => { + return write_http_response( + stream, + 400, + "application/json", + &serde_json::json!({"error": error.to_string()}).to_string(), + ); + } + }; + let requester = stream.peer_addr().ok().map(|addr| addr.ip().to_string()); + let defaults = cfg.computer_use_dispatch_defaults(); + let request = match session::manager::create_computer_use_remote_dispatch_request( + db, + cfg, + &payload.goal, + payload.target_url.as_deref(), + payload.context.as_deref(), + target_session_id.as_deref(), + payload.priority.unwrap_or(TaskPriorityArg::Normal).into(), + payload.agent.as_deref(), + payload.profile.as_deref(), + Some(payload.use_worktree.unwrap_or(defaults.use_worktree)), + session::SessionGrouping { + project: payload.project, + task_group: payload.task_group, + }, + "http_computer_use", + requester.as_deref(), + ) { + Ok(request) => request, + Err(error) => { + return write_http_response( + stream, + 400, + "application/json", + &serde_json::json!({"error": error.to_string()}).to_string(), + ); + } + }; + + write_http_response( + stream, + 202, + "application/json", + &serde_json::to_string(&request)?, + ) + } _ => write_http_response( stream, 404, @@ -4995,6 +5210,55 @@ mod tests { } } + #[test] + fn cli_parses_remote_computer_use_command() { + let cli = Cli::try_parse_from([ + "ecc", + "remote", + "computer-use", + "--goal", + "Confirm the recovery banner", + "--target-url", + "https://ecc.tools/account", + "--context", + "Use the production flow", + "--priority", + "critical", + "--agent", + "codex", + "--profile", + "browser", + "--no-worktree", + ]) + .expect("remote computer-use should parse"); + + match cli.command { + Some(Commands::Remote { + command: + RemoteCommands::ComputerUse { + goal, + target_url, + context, + priority, + agent, + profile, + worktree, + .. + }, + }) => { + assert_eq!(goal, "Confirm the recovery banner"); + assert_eq!(target_url.as_deref(), Some("https://ecc.tools/account")); + assert_eq!(context.as_deref(), Some("Use the production flow")); + assert_eq!(priority, TaskPriorityArg::Critical); + assert_eq!(agent.as_deref(), Some("codex")); + assert_eq!(profile.as_deref(), Some("browser")); + assert!(worktree.no_worktree); + assert!(!worktree.worktree); + } + _ => panic!("expected remote computer-use subcommand"), + } + } + #[test] fn cli_parses_start_with_handoff_source() { let cli = Cli::try_parse_from([ diff --git a/ecc2/src/session/manager.rs b/ecc2/src/session/manager.rs index e2dccfe5..5c6d4e30 100644 --- a/ecc2/src/session/manager.rs +++ b/ecc2/src/session/manager.rs @@ -14,8 +14,8 @@ use super::runtime::capture_command_output; use super::store::StateStore; use super::{ default_project_label, default_task_group_label, normalize_group_label, HarnessKind, - ScheduledTask, Session, SessionAgentProfile, SessionGrouping, SessionHarnessInfo, - SessionMetrics, SessionState, + RemoteDispatchKind, ScheduledTask, Session, SessionAgentProfile, SessionGrouping, + SessionHarnessInfo, SessionMetrics, SessionState, }; use crate::comms::{self, MessageType, TaskPriority}; use crate::config::Config; @@ -268,6 +268,125 @@ pub fn create_remote_dispatch_request( ) -> Result { let working_dir = std::env::current_dir().context("Failed to resolve current working directory")?; + create_remote_dispatch_request_inner( + db, + cfg, + RemoteDispatchKind::Standard, + &working_dir, + task, + None, + target_session_id, + priority, + agent_type, + profile_name, + use_worktree, + grouping, + source, + requester, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn create_computer_use_remote_dispatch_request( + db: &StateStore, + cfg: &Config, + goal: &str, + target_url: Option<&str>, + context: Option<&str>, + target_session_id: Option<&str>, + priority: TaskPriority, + agent_type_override: Option<&str>, + profile_name_override: Option<&str>, + use_worktree_override: Option, + grouping: SessionGrouping, + source: &str, + requester: Option<&str>, +) -> Result { + let working_dir = + std::env::current_dir().context("Failed to resolve current working directory")?; + create_computer_use_remote_dispatch_request_in_dir( + db, + cfg, + &working_dir, + goal, + target_url, + context, + target_session_id, + priority, + agent_type_override, + profile_name_override, + use_worktree_override, + grouping, + source, + requester, + ) +} + +#[allow(clippy::too_many_arguments)] +fn create_computer_use_remote_dispatch_request_in_dir( + db: &StateStore, + cfg: &Config, + working_dir: &Path, + goal: &str, + target_url: Option<&str>, + context: Option<&str>, + target_session_id: Option<&str>, + priority: TaskPriority, + agent_type_override: Option<&str>, + profile_name_override: Option<&str>, + use_worktree_override: Option, + grouping: SessionGrouping, + source: &str, + requester: Option<&str>, +) -> Result { + let defaults = cfg.computer_use_dispatch_defaults(); + let task = render_computer_use_task(goal, target_url, context); + let agent_type = agent_type_override.unwrap_or(&defaults.agent); + let profile_name = profile_name_override.or(defaults.profile.as_deref()); + let use_worktree = use_worktree_override.unwrap_or(defaults.use_worktree); + let grouping = SessionGrouping { + project: grouping.project.or(defaults.project), + task_group: grouping + .task_group + .or(defaults.task_group) + .or_else(|| Some(default_task_group_label(goal))), + }; + + create_remote_dispatch_request_inner( + db, + cfg, + RemoteDispatchKind::ComputerUse, + working_dir, + &task, + target_url, + target_session_id, + priority, + agent_type, + profile_name, + use_worktree, + grouping, + source, + requester, + ) +} + +#[allow(clippy::too_many_arguments)] +fn create_remote_dispatch_request_inner( + db: &StateStore, + cfg: &Config, + request_kind: RemoteDispatchKind, + working_dir: &Path, + task: &str, + target_url: Option<&str>, + target_session_id: Option<&str>, + priority: TaskPriority, + agent_type: &str, + profile_name: Option<&str>, + use_worktree: bool, + grouping: SessionGrouping, + source: &str, + requester: Option<&str>, +) -> Result { let project = grouping .project .as_deref() @@ -288,8 +407,10 @@ pub fn create_remote_dispatch_request( } db.insert_remote_dispatch_request( + request_kind, target_session_id, task, + target_url, priority, &agent_type, profile_name, @@ -302,6 +423,24 @@ pub fn create_remote_dispatch_request( ) } +fn render_computer_use_task(goal: &str, target_url: Option<&str>, context: Option<&str>) -> String { + let mut lines = vec![ + "Computer-use task.".to_string(), + format!("Goal: {}", goal.trim()), + ]; + if let Some(target_url) = target_url.map(str::trim).filter(|value| !value.is_empty()) { + lines.push(format!("Target URL: {target_url}")); + } + if let Some(context) = context.map(str::trim).filter(|value| !value.is_empty()) { + lines.push(format!("Context: {context}")); + } + lines.push( + "Use browser or computer-use tools directly when available, and report blockers clearly if auth, approvals, or local-device access prevent completion." + .to_string(), + ); + lines.join("\n") +} + pub fn list_remote_dispatch_requests( db: &StateStore, include_processed: bool, @@ -3840,6 +3979,7 @@ mod tests { agent_profiles: Default::default(), orchestration_templates: Default::default(), memory_connectors: Default::default(), + computer_use_dispatch: crate::config::ComputerUseDispatchConfig::default(), auto_dispatch_unread_handoffs: false, auto_dispatch_limit_per_session: 5, auto_create_worktrees: true, @@ -4656,8 +4796,10 @@ mod tests { let (fake_runner, _log_path) = write_fake_claude(tempdir.path())?; let request = db.insert_remote_dispatch_request( + RemoteDispatchKind::Standard, None, "Remote phone triage", + None, TaskPriority::High, "claude", None, @@ -4703,6 +4845,59 @@ mod tests { Ok(()) } + #[test] + fn create_computer_use_remote_dispatch_request_uses_config_defaults() -> Result<()> { + let tempdir = TestDir::new("manager-create-computer-use-remote-defaults")?; + let repo_root = tempdir.path().join("repo"); + init_git_repo(&repo_root)?; + + let mut cfg = build_config(tempdir.path()); + cfg.computer_use_dispatch = crate::config::ComputerUseDispatchConfig { + agent: Some("codex".to_string()), + profile: None, + use_worktree: false, + project: Some("ops".to_string()), + task_group: Some("remote browser".to_string()), + }; + let db = StateStore::open(&cfg.db_path)?; + + let request = create_computer_use_remote_dispatch_request_in_dir( + &db, + &cfg, + &repo_root, + "Open the billing portal and confirm the refund banner", + Some("https://ecc.tools/account"), + Some("Use the production account flow"), + None, + TaskPriority::Critical, + None, + None, + None, + SessionGrouping::default(), + "http_computer_use", + Some("127.0.0.1"), + )?; + + assert_eq!(request.request_kind, RemoteDispatchKind::ComputerUse); + assert_eq!( + request.target_url.as_deref(), + Some("https://ecc.tools/account") + ); + assert_eq!(request.agent_type, "codex"); + assert_eq!(request.project, "ops"); + assert_eq!(request.task_group, "remote browser"); + assert!(!request.use_worktree); + assert!(request.task.contains("Computer-use task.")); + assert!(request.task.contains("Goal: Open the billing portal")); + assert!(request + .task + .contains("Target URL: https://ecc.tools/account")); + assert!(request + .task + .contains("Context: Use the production account flow")); + Ok(()) + } + #[tokio::test(flavor = "current_thread")] async fn stop_session_kills_process_and_optionally_cleans_worktree() -> Result<()> { let tempdir = TestDir::new("manager-stop-session")?; diff --git a/ecc2/src/session/mod.rs b/ecc2/src/session/mod.rs index 5175d9d9..1f53a6fb 100644 --- a/ecc2/src/session/mod.rs +++ b/ecc2/src/session/mod.rs @@ -398,8 +398,10 @@ pub struct ScheduledTask { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct RemoteDispatchRequest { pub id: i64, + pub request_kind: RemoteDispatchKind, pub target_session_id: Option, pub task: String, + pub target_url: Option, pub priority: crate::comms::TaskPriority, pub agent_type: String, pub profile_name: Option, @@ -418,6 +420,31 @@ pub struct RemoteDispatchRequest { pub dispatched_at: Option>, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum RemoteDispatchKind { + Standard, + ComputerUse, +} + +impl fmt::Display for RemoteDispatchKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Standard => write!(f, "standard"), + Self::ComputerUse => write!(f, "computer_use"), + } + } +} + +impl RemoteDispatchKind { + pub fn from_db_value(value: &str) -> Self { + match value { + "computer_use" => Self::ComputerUse, + _ => Self::Standard, + } + } +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum RemoteDispatchStatus { diff --git a/ecc2/src/session/store.rs b/ecc2/src/session/store.rs index d23187c9..6d808784 100644 --- a/ecc2/src/session/store.rs +++ b/ecc2/src/session/store.rs @@ -18,8 +18,8 @@ use super::{ ContextGraphCompactionStats, ContextGraphEntity, ContextGraphEntityDetail, ContextGraphObservation, ContextGraphRecallEntry, ContextGraphRelation, ContextGraphSyncStats, ContextObservationPriority, DecisionLogEntry, FileActivityAction, FileActivityEntry, - HarnessKind, RemoteDispatchRequest, RemoteDispatchStatus, ScheduledTask, Session, - SessionAgentProfile, SessionHarnessInfo, SessionMessage, SessionMetrics, SessionState, + HarnessKind, RemoteDispatchKind, RemoteDispatchRequest, RemoteDispatchStatus, ScheduledTask, + Session, SessionAgentProfile, SessionHarnessInfo, SessionMessage, SessionMetrics, SessionState, WorktreeInfo, }; @@ -318,8 +318,10 @@ impl StateStore { CREATE TABLE IF NOT EXISTS remote_dispatch_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, + request_kind TEXT NOT NULL DEFAULT 'standard', target_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL, task TEXT NOT NULL, + target_url TEXT, priority INTEGER NOT NULL DEFAULT 1, agent_type TEXT NOT NULL, profile_name TEXT, @@ -681,6 +683,24 @@ impl StateStore { .context("Failed to add last_auto_prune_active_skipped column to daemon_activity table")?; } + if !self.has_column("remote_dispatch_requests", "request_kind")? { + self.conn + .execute( + "ALTER TABLE remote_dispatch_requests ADD COLUMN request_kind TEXT NOT NULL DEFAULT 'standard'", + [], + ) + .context("Failed to add request_kind column to remote_dispatch_requests table")?; + } + + if !self.has_column("remote_dispatch_requests", "target_url")? { + self.conn + .execute( + "ALTER TABLE remote_dispatch_requests ADD COLUMN target_url TEXT", + [], + ) + .context("Failed to add target_url column to remote_dispatch_requests table")?; + } + self.conn.execute_batch( "CREATE UNIQUE INDEX IF NOT EXISTS idx_tool_log_hook_event ON tool_log(hook_event_id) @@ -1192,8 +1212,10 @@ impl StateStore { #[allow(clippy::too_many_arguments)] pub fn insert_remote_dispatch_request( &self, + request_kind: RemoteDispatchKind, target_session_id: Option<&str>, task: &str, + target_url: Option<&str>, priority: crate::comms::TaskPriority, agent_type: &str, profile_name: Option<&str>, @@ -1207,8 +1229,10 @@ impl StateStore { let now = chrono::Utc::now(); self.conn.execute( "INSERT INTO remote_dispatch_requests ( + request_kind, target_session_id, task, + target_url, priority, agent_type, profile_name, @@ -1221,10 +1245,12 @@ impl StateStore { status, created_at, updated_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, 'pending', ?12, ?13)", + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, 'pending', ?14, ?15)", rusqlite::params![ + request_kind.to_string(), target_session_id, task, + target_url, task_priority_db_value(priority), agent_type, profile_name, @@ -1250,7 +1276,7 @@ impl StateStore { limit: usize, ) -> Result> { let sql = if include_processed { - "SELECT id, target_session_id, task, priority, agent_type, profile_name, working_dir, + "SELECT id, request_kind, target_session_id, task, target_url, priority, agent_type, profile_name, working_dir, project, task_group, use_worktree, source, requester, status, result_session_id, result_action, error, created_at, updated_at, dispatched_at FROM remote_dispatch_requests @@ -1258,7 +1284,7 @@ impl StateStore { priority DESC, created_at ASC, id ASC LIMIT ?1" } else { - "SELECT id, target_session_id, task, priority, agent_type, profile_name, working_dir, + "SELECT id, request_kind, target_session_id, task, target_url, priority, agent_type, profile_name, working_dir, project, task_group, use_worktree, source, requester, status, result_session_id, result_action, error, created_at, updated_at, dispatched_at FROM remote_dispatch_requests @@ -1285,7 +1311,7 @@ impl StateStore { ) -> Result> { self.conn .query_row( - "SELECT id, target_session_id, task, priority, agent_type, profile_name, working_dir, + "SELECT id, request_kind, target_session_id, task, target_url, priority, agent_type, profile_name, working_dir, project, task_group, use_worktree, source, requester, status, result_session_id, result_action, error, created_at, updated_at, dispatched_at FROM remote_dispatch_requests @@ -3900,29 +3926,31 @@ fn map_scheduled_task(row: &rusqlite::Row<'_>) -> rusqlite::Result) -> rusqlite::Result { - let created_at = parse_store_timestamp(row.get::<_, String>(16)?, 16)?; - let updated_at = parse_store_timestamp(row.get::<_, String>(17)?, 17)?; + let created_at = parse_store_timestamp(row.get::<_, String>(18)?, 18)?; + let updated_at = parse_store_timestamp(row.get::<_, String>(19)?, 19)?; let dispatched_at = row - .get::<_, Option>(18)? - .map(|value| parse_store_timestamp(value, 18)) + .get::<_, Option>(20)? + .map(|value| parse_store_timestamp(value, 20)) .transpose()?; Ok(RemoteDispatchRequest { id: row.get(0)?, - target_session_id: normalize_optional_string(row.get(1)?), - task: row.get(2)?, - priority: task_priority_from_db_value(row.get::<_, i64>(3)?), - agent_type: row.get(4)?, - profile_name: normalize_optional_string(row.get(5)?), - working_dir: PathBuf::from(row.get::<_, String>(6)?), - project: row.get(7)?, - task_group: row.get(8)?, - use_worktree: row.get::<_, i64>(9)? != 0, - source: row.get(10)?, - requester: normalize_optional_string(row.get(11)?), - status: RemoteDispatchStatus::from_db_value(&row.get::<_, String>(12)?), - result_session_id: normalize_optional_string(row.get(13)?), - result_action: normalize_optional_string(row.get(14)?), - error: normalize_optional_string(row.get(15)?), + request_kind: RemoteDispatchKind::from_db_value(&row.get::<_, String>(1)?), + target_session_id: normalize_optional_string(row.get(2)?), + task: row.get(3)?, + target_url: normalize_optional_string(row.get(4)?), + priority: task_priority_from_db_value(row.get::<_, i64>(5)?), + agent_type: row.get(6)?, + profile_name: normalize_optional_string(row.get(7)?), + working_dir: PathBuf::from(row.get::<_, String>(8)?), + project: row.get(9)?, + task_group: row.get(10)?, + use_worktree: row.get::<_, i64>(11)? != 0, + source: row.get(12)?, + requester: normalize_optional_string(row.get(13)?), + status: RemoteDispatchStatus::from_db_value(&row.get::<_, String>(14)?), + result_session_id: normalize_optional_string(row.get(15)?), + result_action: normalize_optional_string(row.get(16)?), + error: normalize_optional_string(row.get(17)?), created_at, updated_at, dispatched_at, diff --git a/ecc2/src/tui/dashboard.rs b/ecc2/src/tui/dashboard.rs index c2f94056..ebdf2e53 100644 --- a/ecc2/src/tui/dashboard.rs +++ b/ecc2/src/tui/dashboard.rs @@ -14612,6 +14612,7 @@ diff --git a/src/lib.rs b/src/lib.rs agent_profiles: Default::default(), orchestration_templates: Default::default(), memory_connectors: Default::default(), + computer_use_dispatch: crate::config::ComputerUseDispatchConfig::default(), auto_dispatch_unread_handoffs: false, auto_dispatch_limit_per_session: 5, auto_create_worktrees: true,