mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-16 23:23:29 +08:00
feat: add ecc2 computer use remote dispatch
This commit is contained in:
@@ -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<super::RemoteDispatchRequest> {
|
||||
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<bool>,
|
||||
grouping: SessionGrouping,
|
||||
source: &str,
|
||||
requester: Option<&str>,
|
||||
) -> Result<super::RemoteDispatchRequest> {
|
||||
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<bool>,
|
||||
grouping: SessionGrouping,
|
||||
source: &str,
|
||||
requester: Option<&str>,
|
||||
) -> Result<super::RemoteDispatchRequest> {
|
||||
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<super::RemoteDispatchRequest> {
|
||||
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")?;
|
||||
|
||||
@@ -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<String>,
|
||||
pub task: String,
|
||||
pub target_url: Option<String>,
|
||||
pub priority: crate::comms::TaskPriority,
|
||||
pub agent_type: String,
|
||||
pub profile_name: Option<String>,
|
||||
@@ -418,6 +420,31 @@ pub struct RemoteDispatchRequest {
|
||||
pub dispatched_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
|
||||
@@ -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<Vec<RemoteDispatchRequest>> {
|
||||
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<Option<RemoteDispatchRequest>> {
|
||||
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<ScheduledTask
|
||||
}
|
||||
|
||||
fn map_remote_dispatch_request(row: &rusqlite::Row<'_>) -> rusqlite::Result<RemoteDispatchRequest> {
|
||||
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<String>>(18)?
|
||||
.map(|value| parse_store_timestamp(value, 18))
|
||||
.get::<_, Option<String>>(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,
|
||||
|
||||
Reference in New Issue
Block a user