Skip to content

Commit 641b90c

Browse files
committed
merge: retain archive deadline and declared-directory checkpoint tests
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
2 parents 55bb3c7 + 937a011 commit 641b90c

114 files changed

Lines changed: 3574 additions & 331 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/PULL_REQUEST_TEMPLATE.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,21 @@ after view. Include desktop and mobile when responsive layout changes, and inclu
110110
loading, empty, error, permission, or gated states when those states changed. Add
111111
a short recording only when still images cannot explain the interaction.
112112
113+
Put the first viewport screenshot directly in this description, with the tested
114+
revision, data/build boundary and a working preview link when available. Refresh
115+
it for materially changed presentation. Ordinary authorized implementation and
116+
refinement do not require another user-confirmation round; reuse prior scoped
117+
direction. Ask only for an explicitly reserved presentation decision or a real
118+
scope/authority gap. See AGENTS.md's First-Screen Review Evidence rule.
119+
113120
Use synthetic or public fixture data only. Do not upload live/private screenshots,
114121
even when the underlying access was authorized.
115122
-->
116123

117124
- UI impact: <!-- choose one: none | changed -->
118125
- Before:
119126
- After:
127+
- First viewport and preview: <!-- Embed the screenshot here; identify revision and prototype/packaged/installed boundary. N/A for no UI impact. -->
120128
- States and viewports shown:
121129
- Source data: <!-- choose one: none | synthetic | public_fixture -->
122130
- Attention review: <!-- Per docs/development/design.md: what earns its place through high-value information, essential interaction, or expressive visual presentation? Name consolidation/removal across the whole viewport, and how failures, uncertainty, and one-step controls remain available. -->

‎AGENTS.md‎

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ read back its state and unchanged head, and refresh the review packet. Drafts
102102
are excluded from the review queue, so waiting for a review before marking one
103103
ready creates a deadlock. A reviewer must still obey the packet's action
104104
authority and may not mark another author's Draft ready without authorization.
105-
Keep exact-head review, required validation, first-screen approval and explicit
105+
Keep exact-head review, required validation, first-screen evidence and explicit
106106
merge authorization as their separate gates.
107107

108108
### DCO Sign-Off
@@ -278,20 +278,34 @@ the validator's explicit bilingual no-change declarations. Never treat the
278278
existence of a checklist, a release PR draft, or architecture-only capability
279279
copy as proof that the final release body is usable.
280280

281-
## First-Screen Review Gate
282-
283-
Treat the first visible screen of public product surfaces as owner-reviewed
284-
presentation, not as ordinary copy. Before committing, pushing, or self-merging
285-
changes that alter the first viewport, hero block, primary CTA, or opening
286-
navigation of README, hosted frontstage, showcase index pages, product home
287-
pages, or similarly prominent public entry points, show the user a preview
288-
first and wait for approval.
289-
290-
The preview should be concrete enough to judge the presentation: provide the
291-
local URL and, when the surface is visual HTML, a screenshot or browser view of
292-
the first viewport. Do not move the review gate into a PR comment, todo note, or
293-
final summary after the fact. It must happen before the public first-screen
294-
change is finalized.
281+
## First-Screen Review Evidence
282+
283+
Treat the first visible screen of public product surfaces as reviewable product
284+
presentation. For authorized work within the requested outcome and existing
285+
product/design contract, do not add a separate user-confirmation round before commit,
286+
push or PR submission merely because the first viewport, hero, primary CTA or
287+
opening navigation changes. Reuse prior scoped approval; a new commit or a
288+
routine refinement does not invalidate it.
289+
290+
Before submitting or updating the PR, inspect the actual first viewport and put
291+
its screenshot or browser capture directly in the PR description's visual
292+
evidence section. Include a useful before/after comparison for an existing
293+
surface, affected desktop/narrow views, and a working preview link when one is
294+
available. Identify the tested revision and whether the data and surface are
295+
synthetic, a prototype, packaged or installed. Use public-safe evidence; keep
296+
private messages, paths and live operating context out. A screenshot only in
297+
chat, a PR comment, a Todo note or the final report does not satisfy this rule.
298+
Refresh the evidence when the visible result materially changes, without
299+
requesting another approval for the same authorized direction.
300+
301+
Ask only for a genuinely missing consequential decision, work outside the
302+
accepted scope, or an explicit user instruction reserving presentation approval.
303+
Do not infer such a reservation from a first-screen change alone. Name the
304+
specific unresolved choice and continue independent authorized work. This
305+
presentation evidence rule does not grant merge, installation, release, paid
306+
execution or sensitive-data publication authority. Existing approval records
307+
remain historical evidence; older prose that merely repeats the blanket
308+
first-screen confirmation requirement follows this current rule.
295309

296310
## Product Delivery Completeness
297311

‎apps/desktop/loopx-control-plane/README.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ cargo run --example webcontent_recovery -- --baseline
153153
```
154154

155155
It reads no Goals or accounts and never targets an installed LoopX process.
156+
156157
The fault injector uses a private WebKit process-id getter only in this example;
157158
production recovery uses the public Tauri termination hook.
158159
This controlled regression does not establish long-duration reliability or
@@ -188,6 +189,33 @@ rechecked at most every 30 seconds; an explicit recovery action wakes the
188189
supervisor immediately. It returns to the loading shape as soon as the
189190
snapshot leaves the terminal phase (for example while an explicit repair runs).
190191

192+
### Returning After A Normal Close
193+
194+
The packaged App remembers the selected local Goal, view, Agent and Todo when
195+
its main window closes or the App quits. Reopening uses the newly allocated
196+
loopback origin and returns to that selection without another navigation step.
197+
Selecting **LoopX 管家** before closing instead returns to Manager next time.
198+
This restores a reading position; it never submits a message or starts work.
199+
200+
The optional `workspace-location.json` in the App's private data directory
201+
contains only those selection identifiers. It stores no origin, raw URL, prompt,
202+
credentials or report/session query. Missing, corrupt or obsolete preferences
203+
fall back to the ordinary workspace; unknown views fall back to the frontend's
204+
default. Boot and foreign pages cannot overwrite a saved local selection.
205+
Remote status sources and host-granted workspace contexts are not restored:
206+
they reopen at local Manager, so their Goal identifiers cannot be replayed
207+
against the local source. Vite development navigation is unchanged. Explicit
208+
runtime repair retains its existing root handoff; WebContent crash recovery
209+
continues to retain the current in-process route.
210+
211+
For native qualification, use a separately identified packaged App with the
212+
real backend: select a local Goal and Agent page, quit normally, reopen, and
213+
check the same selection on the new port. Also check window-close, an explicit
214+
return to Manager and a corrupt preference in that disposable App profile.
215+
Retain Session IDs and active-Turn readbacks before and after; navigation is
216+
not evidence that work was resumed or accepted. Cross-platform and sustained
217+
use require their own qualification.
218+
191219
## Known Issues
192220

193221
### Fresh Mac without a usable Python stays on the boot screen

‎apps/desktop/loopx-control-plane/src-tauri/src/lib.rs‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod service_endpoints;
77
mod services;
88
mod update_backup;
99
mod workspace_handoff;
10+
mod workspace_location;
1011

1112
use services::ServiceSet;
1213
use std::sync::{
@@ -15,7 +16,7 @@ use std::sync::{
1516
};
1617
use tauri::{
1718
ipc::CapabilityBuilder, webview::PageLoadEvent, AppHandle, Manager, RunEvent, Url, WebviewUrl,
18-
WebviewWindowBuilder,
19+
WebviewWindowBuilder, WindowEvent,
1920
};
2021
use tauri_plugin_notification::NotificationExt;
2122
use workspace_handoff::{NavigationRetry, WorkspaceHandoff};
@@ -135,6 +136,11 @@ pub fn run() {
135136
.build(),
136137
)
137138
.plugin(tauri_plugin_notification::init())
139+
.on_window_event(|window, event| {
140+
if window.label() == "main" && matches!(event, WindowEvent::CloseRequested { .. }) {
141+
workspace_location::remember_current(window.app_handle());
142+
}
143+
})
138144
.setup(move |app| {
139145
let origin: Url = web_origin.parse()?;
140146
app.add_capability(
@@ -155,7 +161,7 @@ pub fn run() {
155161
};
156162
let handoff = Arc::new(Mutex::new(WorkspaceHandoff::new(
157163
boot_url(app.handle()),
158-
origin.clone(),
164+
workspace_location::launch_destination(app.handle(), &origin),
159165
retry,
160166
)));
161167
app.manage(Arc::clone(&handoff));
@@ -220,7 +226,10 @@ pub fn run() {
220226
}
221227
let healed = started.healed;
222228
*services_for_setup.lock().expect("service state lock") = Some(started);
223-
handoff.lock().expect("workspace handoff lock").reconnect();
229+
handoff
230+
.lock()
231+
.expect("workspace handoff lock")
232+
.services_ready();
224233
WorkspaceHandoff::reconcile(&handoff, &handle);
225234
if healed {
226235
let _ = handle
@@ -279,8 +288,9 @@ pub fn run() {
279288
let app = builder
280289
.build(tauri::generate_context!())
281290
.expect("failed to build LoopX desktop shell");
282-
app.run(move |_app, event| {
291+
app.run(move |app, event| {
283292
if matches!(event, RunEvent::Exit | RunEvent::ExitRequested { .. }) {
293+
workspace_location::remember_current(app);
284294
shutting_down.store(true, Ordering::Release);
285295
if let Ok(mut guard) = services.lock() {
286296
if let Some(current) = guard.as_mut() {

‎apps/desktop/loopx-control-plane/src-tauri/src/workspace_handoff.rs‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub(crate) enum NavigationRetry {
1616
pub(crate) struct WorkspaceHandoff {
1717
boot_url: Url,
1818
workspace_url: Url,
19+
launch_workspace_url: Option<Url>,
1920
last_workspace_url: Option<Url>,
2021
retry: NavigationRetry,
2122
pending: bool,
@@ -25,9 +26,13 @@ pub(crate) struct WorkspaceHandoff {
2526
impl WorkspaceHandoff {
2627
const RETRY_INTERVAL: Duration = Duration::from_secs(2);
2728
pub(crate) fn new(boot_url: Url, workspace_url: Url, retry: NavigationRetry) -> Self {
29+
let mut root = workspace_url.clone();
30+
root.set_query(None);
31+
root.set_fragment(None);
2832
Self {
2933
boot_url,
30-
workspace_url,
34+
workspace_url: root,
35+
launch_workspace_url: Some(workspace_url),
3136
last_workspace_url: None,
3237
retry,
3338
pending: false,
@@ -36,10 +41,21 @@ impl WorkspaceHandoff {
3641
}
3742
pub(crate) fn reconnect(&mut self) {
3843
// Explicit runtime repair retains its established workspace-root handoff.
44+
self.launch_workspace_url = None;
3945
self.last_workspace_url = None;
4046
self.pending = true;
4147
self.last_attempt = None;
4248
}
49+
pub(crate) fn services_ready(&mut self) {
50+
// A normal launch consumes the saved selection once. Explicit runtime
51+
// repair still returns to the root; it must not replay an old selection.
52+
let launch = self.launch_workspace_url.take();
53+
self.reconnect();
54+
self.last_workspace_url = launch;
55+
}
56+
pub(crate) fn origin(&self) -> &Url {
57+
&self.workspace_url
58+
}
4359
pub(crate) fn page_reached(&mut self, current: &Url) {
4460
if current.origin() == self.workspace_url.origin() {
4561
self.last_workspace_url = Some(current.clone());
@@ -128,6 +144,26 @@ pub(crate) fn content_lost(webview: &tauri::Webview) {
128144
mod tests {
129145
use super::*;
130146
#[test]
147+
fn normal_launch_resumes_once_but_explicit_repair_keeps_its_root_handoff() {
148+
let launch: Url = "http://127.0.0.1:49123/chat/?goalId=example&view=tasks"
149+
.parse()
150+
.unwrap();
151+
let root: Url = "http://127.0.0.1:49123/chat/".parse().unwrap();
152+
let mut h = WorkspaceHandoff::new(
153+
"tauri://localhost/".parse().unwrap(),
154+
launch.clone(),
155+
NavigationRetry::UntilNativeCommit,
156+
);
157+
assert!(!h.needs_navigation(Instant::now()));
158+
h.services_ready();
159+
assert_eq!(h.destination(), &launch);
160+
h.page_reached(&launch);
161+
h.reconnect();
162+
assert_eq!(h.destination(), &root);
163+
h.services_ready();
164+
assert_eq!(h.destination(), &root);
165+
}
166+
#[test]
131167
fn failed_workspace_load_retries_until_a_native_commit() {
132168
let boot: Url = "tauri://localhost/".parse().unwrap();
133169
let target: Url = "http://127.0.0.1:8767/chat/".parse().unwrap();

0 commit comments

Comments
 (0)