diff --git a/apps/desktop/loopx-control-plane/README.md b/apps/desktop/loopx-control-plane/README.md index abce5b11d0..b84976f709 100644 --- a/apps/desktop/loopx-control-plane/README.md +++ b/apps/desktop/loopx-control-plane/README.md @@ -303,6 +303,14 @@ against the local source. Vite development navigation is unchanged. Explicit runtime repair retains its existing root handoff; WebContent crash recovery continues to retain the current in-process route. +Research uses this same saved-view path. The macOS capture below is a packaged +QA build with the real backend and a disposable synthetic Goal, reopened +directly in Research after a normal quit and a loopback-port change. Window +close/reopen, an explicit return to Manager and a corrupt preference were also +checked. It does not demonstrate model execution or an official App upgrade. + +![Packaged QA reopening the same synthetic Goal in Research](docs/assets/research-reopen-qa.jpg) + For native qualification, use a separately identified packaged App with the real backend: select a local Goal and Agent page, quit normally, reopen, and check the same selection on the new port. Also check window-close, an explicit diff --git a/apps/desktop/loopx-control-plane/docs/assets/research-reopen-qa.jpg b/apps/desktop/loopx-control-plane/docs/assets/research-reopen-qa.jpg new file mode 100644 index 0000000000..d9448e1f0f Binary files /dev/null and b/apps/desktop/loopx-control-plane/docs/assets/research-reopen-qa.jpg differ diff --git a/apps/desktop/loopx-control-plane/src-tauri/src/services.rs b/apps/desktop/loopx-control-plane/src-tauri/src/services.rs index 4750c54531..ee5118b4f7 100644 --- a/apps/desktop/loopx-control-plane/src-tauri/src/services.rs +++ b/apps/desktop/loopx-control-plane/src-tauri/src/services.rs @@ -958,13 +958,20 @@ pub(crate) fn node_environment() -> (bool, Option) { let version = executable.and_then(|node| { let mut probe = Command::new(node); probe.arg("--version"); - timed_output(probe).filter(|output| output.status.success()).and_then(|output| { - let text = std::str::from_utf8(&output.stdout).ok()?.trim().strip_prefix('v')?; - let fields: Vec<_> = text.split('.').collect(); - (fields.len() == 3 && fields.iter().all(|field| - !field.is_empty() && field.bytes().all(|byte| byte.is_ascii_digit()))) + timed_output(probe) + .filter(|output| output.status.success()) + .and_then(|output| { + let text = std::str::from_utf8(&output.stdout) + .ok()? + .trim() + .strip_prefix('v')?; + let fields: Vec<_> = text.split('.').collect(); + (fields.len() == 3 + && fields.iter().all(|field| { + !field.is_empty() && field.bytes().all(|byte| byte.is_ascii_digit()) + })) .then(|| text.to_owned()) - }) + }) }); (found, version) } diff --git a/apps/desktop/loopx-control-plane/src-tauri/src/workspace_location.rs b/apps/desktop/loopx-control-plane/src-tauri/src/workspace_location.rs index 30ec5a3345..2a17e5ae6d 100644 --- a/apps/desktop/loopx-control-plane/src-tauri/src/workspace_location.rs +++ b/apps/desktop/loopx-control-plane/src-tauri/src/workspace_location.rs @@ -37,8 +37,11 @@ impl LocalWorkspace { goal_id: selection_id(goal)?, // Existing local presentation vocabulary, owned by dashboard's // router.tsx. Unknown/retired views use the frontend's default. - view: matches!(view, "conversation" | "overview" | "tasks" | "files") - .then(|| view.to_owned()), + view: matches!( + view, + "conversation" | "overview" | "tasks" | "research" | "files" + ) + .then(|| view.to_owned()), work_agent: selection_id(agent), todo: selection_id(todo), }) @@ -211,27 +214,29 @@ mod tests { #[test] fn ordinary_reopen_keeps_selection_across_ports_without_replaying_other_inputs() { let old = origin(49123); - let mut page = old.clone(); - page.query_pairs_mut().extend_pairs([ - ("goalId", "example"), - ("view", "conversation"), - ("workAgent", "worker"), - ("todo", "todo_123"), - ("statusUrl", "/status.json"), - ("token", "secret"), - ("prompt", "run"), - ("reportSessionId", "report"), - ]); - page.set_fragment(Some("secret")); - let selection = LocalWorkspace::capture(&page, &old).unwrap(); let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("app/workspace-location.json"); - write(&path, &selection).unwrap(); - let reopened = read(&path).unwrap().destination(&origin(59234)); - assert_eq!(reopened.as_str(), "http://127.0.0.1:59234/chat/?goalId=example&statusUrl=%2Fstatus.json&view=conversation&workAgent=worker&todo=todo_123"); - let stored = fs::read_to_string(&path).unwrap(); - for excluded in ["secret", "49123", "prompt", "report", "token", "http"] { - assert!(!stored.contains(excluded), "{excluded}"); + for view in ["conversation", "overview", "tasks", "research", "files"] { + let mut page = old.clone(); + page.query_pairs_mut().extend_pairs([ + ("goalId", "example"), + ("view", view), + ("workAgent", "worker"), + ("todo", "todo_123"), + ("statusUrl", "/status.json"), + ("token", "secret"), + ("prompt", "run"), + ("reportSessionId", "report"), + ]); + page.set_fragment(Some("secret")); + let selection = LocalWorkspace::capture(&page, &old).unwrap(); + write(&path, &selection).unwrap(); + let reopened = read(&path).unwrap().destination(&origin(59234)); + assert_eq!(reopened.as_str(), format!("http://127.0.0.1:59234/chat/?goalId=example&statusUrl=%2Fstatus.json&view={view}&workAgent=worker&todo=todo_123")); + let stored = fs::read_to_string(&path).unwrap(); + for excluded in ["secret", "49123", "prompt", "report", "token", "http"] { + assert!(!stored.contains(excluded), "{view}: {excluded}"); + } } }