Merge pull request #5 from lfglabs-dev/fixes

ios fix
This commit is contained in:
Thomas Marchand
2025-12-25 12:04:57 +03:00
committed by GitHub
5 changed files with 111 additions and 43 deletions

View File

@@ -66,6 +66,7 @@ import {
Globe,
Code,
FolderOpen,
Trash2,
} from "lucide-react";
import {
OptionList,
@@ -1021,14 +1022,18 @@ export default function ControlClient() {
};
// Handle resuming an interrupted mission
const handleResumeMission = async () => {
const handleResumeMission = async (cleanWorkspace: boolean = false) => {
if (!currentMission || !["interrupted", "blocked"].includes(currentMission.status)) return;
try {
setMissionLoading(true);
const resumed = await resumeMission(currentMission.id);
const resumed = await resumeMission(currentMission.id, cleanWorkspace);
setCurrentMission(resumed);
setShowStatusMenu(false);
toast.success(currentMission.status === "blocked" ? "Continuing mission" : "Mission resumed");
toast.success(
cleanWorkspace
? "Mission resumed with clean workspace"
: (currentMission.status === "blocked" ? "Continuing mission" : "Mission resumed")
);
} catch (err) {
console.error("Failed to resume mission:", err);
toast.error("Failed to resume mission");
@@ -1413,14 +1418,25 @@ export default function ControlClient() {
Mark Failed
</button>
{(currentMission.status === "interrupted" || currentMission.status === "blocked") && (
<button
onClick={handleResumeMission}
disabled={missionLoading}
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-white/70 hover:bg-white/[0.04] disabled:opacity-50"
>
<PlayCircle className="h-4 w-4 text-emerald-400" />
{currentMission.status === "blocked" ? "Continue Mission" : "Resume Mission"}
</button>
<>
<button
onClick={() => handleResumeMission(false)}
disabled={missionLoading}
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-white/70 hover:bg-white/[0.04] disabled:opacity-50"
>
<PlayCircle className="h-4 w-4 text-emerald-400" />
{currentMission.status === "blocked" ? "Continue Mission" : "Resume Mission"}
</button>
<button
onClick={() => handleResumeMission(true)}
disabled={missionLoading}
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-white/70 hover:bg-white/[0.04] disabled:opacity-50"
title="Delete work folder and start fresh"
>
<Trash2 className="h-4 w-4 text-orange-400" />
Clean & {currentMission.status === "blocked" ? "Continue" : "Resume"}
</button>
</>
)}
{currentMission.status !== "active" && currentMission.status !== "interrupted" && currentMission.status !== "blocked" && (
<button
@@ -1739,18 +1755,29 @@ export default function ControlClient() {
)}
</p>
{currentMission.status === "blocked" && (
<button
onClick={handleResumeMission}
disabled={missionLoading}
className="mt-4 inline-flex items-center gap-2 rounded-lg bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-600 transition-colors disabled:opacity-50"
>
{missionLoading ? (
<Loader className="h-4 w-4 animate-spin" />
) : (
<PlayCircle className="h-4 w-4" />
)}
Continue Mission
</button>
<div className="mt-4 flex gap-2">
<button
onClick={() => handleResumeMission(false)}
disabled={missionLoading}
className="inline-flex items-center gap-2 rounded-lg bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-600 transition-colors disabled:opacity-50"
>
{missionLoading ? (
<Loader className="h-4 w-4 animate-spin" />
) : (
<PlayCircle className="h-4 w-4" />
)}
Continue Mission
</button>
<button
onClick={() => handleResumeMission(true)}
disabled={missionLoading}
className="inline-flex items-center gap-2 rounded-lg bg-white/10 border border-white/20 px-4 py-2 text-sm font-medium text-white/70 hover:bg-white/20 hover:text-white transition-colors disabled:opacity-50"
title="Delete work folder and start fresh"
>
<Trash2 className="h-4 w-4" />
Clean & Continue
</button>
</div>
)}
</>
) : (
@@ -2039,7 +2066,7 @@ export default function ControlClient() {
<span className="text-white/50 ml-1"> Agent used all {maxIterations} iterations</span>
</div>
<button
onClick={handleResumeMission}
onClick={() => handleResumeMission(false)}
disabled={missionLoading}
className="ml-2 inline-flex items-center gap-1.5 rounded-lg bg-amber-500 px-3 py-1.5 text-sm font-medium text-black hover:bg-amber-400 transition-colors disabled:opacity-50"
>
@@ -2050,6 +2077,15 @@ export default function ControlClient() {
)}
Continue
</button>
<button
onClick={() => handleResumeMission(true)}
disabled={missionLoading}
className="inline-flex items-center gap-1.5 rounded-lg bg-white/10 border border-white/20 px-3 py-1.5 text-sm font-medium text-white/70 hover:bg-white/20 hover:text-white transition-colors disabled:opacity-50"
title="Delete work folder and start fresh"
>
<Trash2 className="h-3.5 w-3.5" />
Clean & Continue
</button>
</div>
</div>
)}

View File

@@ -397,9 +397,11 @@ export async function setMissionStatus(
}
// Resume an interrupted mission
export async function resumeMission(id: string): Promise<Mission> {
export async function resumeMission(id: string, cleanWorkspace: boolean = false): Promise<Mission> {
const res = await apiFetch(`/api/control/missions/${id}/resume`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ clean_workspace: cleanWorkspace }),
});
if (!res.ok) {
const text = await res.text();

View File

@@ -216,7 +216,7 @@ struct ControlView: View {
Task { await refreshRunningMissions() }
}
)
.transition(.move(edge: .top).combined(with: .opacity))
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
}
// MARK: - Background

View File

@@ -305,6 +305,8 @@ pub enum ControlCommand {
/// Resume an interrupted mission
ResumeMission {
mission_id: Uuid,
/// If true, clean the mission's work directory before resuming
clean_workspace: bool,
respond: oneshot::Sender<Result<Mission, String>>,
},
/// Graceful shutdown - mark running missions as interrupted
@@ -956,12 +958,22 @@ pub async fn cancel_mission(
.map_err(|e| (StatusCode::NOT_FOUND, e))
}
/// Request body for resuming a mission
#[derive(Debug, Deserialize, Default)]
pub struct ResumeMissionRequest {
/// If true, clean the mission's work directory before resuming
#[serde(default)]
pub clean_workspace: bool,
}
/// Resume an interrupted mission.
/// This reconstructs context from history and work directory, then restarts execution.
pub async fn resume_mission(
State(state): State<Arc<AppState>>,
Path(mission_id): Path<Uuid>,
body: Option<Json<ResumeMissionRequest>>,
) -> Result<Json<Mission>, (StatusCode, String)> {
let clean_workspace = body.map(|b| b.clean_workspace).unwrap_or(false);
let (tx, rx) = oneshot::channel();
state
@@ -969,6 +981,7 @@ pub async fn resume_mission(
.cmd_tx
.send(ControlCommand::ResumeMission {
mission_id,
clean_workspace,
respond: tx,
})
.await
@@ -1335,6 +1348,7 @@ async fn control_actor_loop(
memory: &Option<MemorySystem>,
config: &Config,
mission_id: Uuid,
clean_workspace: bool,
) -> Result<(Mission, String), String> {
let mission = load_mission_from_db(memory, mission_id).await?;
@@ -1346,6 +1360,19 @@ async fn control_actor_loop(
));
}
// Clean workspace if requested
let short_id = &mission_id.to_string()[..8];
let mission_dir = config.working_dir.join("work").join(format!("mission-{}", short_id));
if clean_workspace && mission_dir.exists() {
tracing::info!("Cleaning workspace for mission {} at {:?}", mission_id, mission_dir);
if let Err(e) = std::fs::remove_dir_all(&mission_dir) {
tracing::warn!("Failed to clean workspace: {}", e);
}
// Recreate the directory
let _ = std::fs::create_dir_all(&mission_dir);
}
// Build resume context
let mut resume_parts = Vec::new();
@@ -1355,13 +1382,19 @@ async fn control_actor_loop(
_ => "was interrupted",
};
let workspace_note = if clean_workspace {
" (workspace cleaned)"
} else {
""
};
if let Some(interrupted_at) = &mission.interrupted_at {
resume_parts.push(format!(
"**MISSION RESUMED**\nThis mission {} at {} and is now being continued.",
resume_reason, interrupted_at
"**MISSION RESUMED**{}\nThis mission {} at {} and is now being continued.",
workspace_note, resume_reason, interrupted_at
));
} else {
resume_parts.push(format!("**MISSION RESUMED**\nThis mission {} and is now being continued.", resume_reason));
resume_parts.push(format!("**MISSION RESUMED**{}\nThis mission {} and is now being continued.", workspace_note, resume_reason));
}
// Add history summary
@@ -1384,10 +1417,7 @@ async fn control_actor_loop(
}
}
// Scan work directory for artifacts
let short_id = &mission_id.to_string()[..8];
let mission_dir = config.working_dir.join("work").join(format!("mission-{}", short_id));
// Scan work directory for artifacts (use mission_dir defined earlier)
if mission_dir.exists() {
resume_parts.push("\n## Work Directory Contents".to_string());
@@ -1735,9 +1765,9 @@ async fn control_actor_loop(
let _ = respond.send(running_list);
}
ControlCommand::ResumeMission { mission_id, respond } => {
ControlCommand::ResumeMission { mission_id, clean_workspace, respond } => {
// Resume an interrupted mission by building resume context
match resume_mission_impl(&memory, &config, mission_id).await {
match resume_mission_impl(&memory, &config, mission_id, clean_workspace).await {
Ok((mission, resume_prompt)) => {
// First persist current mission history (if any)
persist_mission_history(&memory, &current_mission, &history).await;

View File

@@ -804,7 +804,7 @@ impl Tool for BrowserClick {
}
fn description(&self) -> &str {
"Click on an element in the browser. Use CSS selectors like '#id', '.class', 'button', 'a[href*=login]', etc."
"Click on an element in the browser. Use standard CSS selectors like '#id', '.class', 'button', 'a[href*=login]', etc. NOTE: jQuery pseudo-selectors like :contains() are NOT supported - use XPath or JavaScript for text matching instead."
}
fn parameters_schema(&self) -> Value {
@@ -813,7 +813,7 @@ impl Tool for BrowserClick {
"properties": {
"selector": {
"type": "string",
"description": "CSS selector for the element to click"
"description": "Standard CSS selector (e.g., '#id', '.class', 'button', 'a[href*=download]'). Do NOT use jQuery :contains() - it's not valid CSS."
}
},
"required": ["selector"]
@@ -924,7 +924,7 @@ impl Tool for BrowserEvaluate {
}
fn description(&self) -> &str {
"Execute JavaScript code in the browser and return the result. Useful for complex interactions, extracting data, or debugging."
"Execute JavaScript code in the browser and return the result. Useful for complex interactions, extracting data, or debugging. NOTE: DevTools-only APIs like getEventListeners() are NOT available - use standard DOM APIs only."
}
fn parameters_schema(&self) -> Value {
@@ -933,7 +933,7 @@ impl Tool for BrowserEvaluate {
"properties": {
"script": {
"type": "string",
"description": "JavaScript code to execute. The result of the last expression is returned."
"description": "JavaScript code to execute. Use standard DOM APIs only - DevTools-only functions like getEventListeners() are not available. The result of the last expression is returned."
}
},
"required": ["script"]
@@ -973,7 +973,7 @@ impl Tool for BrowserWait {
}
fn description(&self) -> &str {
"Wait for an element to appear or a condition to be met. Use after clicking or navigating when content loads dynamically."
"Wait for an element to appear or a condition to be met. Use after clicking or navigating when content loads dynamically. NOTE: Use standard CSS selectors only - jQuery pseudo-selectors like :contains() are NOT supported."
}
fn parameters_schema(&self) -> Value {
@@ -982,7 +982,7 @@ impl Tool for BrowserWait {
"properties": {
"selector": {
"type": "string",
"description": "CSS selector to wait for"
"description": "Standard CSS selector to wait for (e.g., '#id', '.class', 'div.loaded'). Do NOT use jQuery :contains()."
},
"timeout_ms": {
"type": "integer",
@@ -1080,7 +1080,7 @@ impl Tool for BrowserListElements {
}
fn description(&self) -> &str {
"List interactive elements on the current page (links, buttons, inputs, etc.). Useful for understanding page structure before interacting."
"List interactive elements on the current page (links, buttons, inputs, etc.). Useful for understanding page structure before interacting. NOTE: Use standard CSS selectors only - jQuery pseudo-selectors like :contains() are NOT supported."
}
fn parameters_schema(&self) -> Value {
@@ -1089,7 +1089,7 @@ impl Tool for BrowserListElements {
"properties": {
"selector": {
"type": "string",
"description": "Optional CSS selector to filter elements (default: 'a, button, input, select, textarea, [onclick]')"
"description": "Standard CSS selector to filter elements (default: 'a, button, input, select, textarea, [onclick]'). Do NOT use jQuery :contains() - it's not valid CSS."
},
"limit": {
"type": "integer",