fix(forms): reset replacement sessions and preserve visible scroll

This commit is contained in:
Codex
2026-09-11 08:33:06 -07:00
parent 8895dd2e0f
commit 80f2588214
10 changed files with 395 additions and 44 deletions
@@ -425,6 +425,25 @@ fn review_reference_facets_groups_and_attachment_hosts_browse_without_replanning
};
let mut app = App::with_project(p.clone());
let _ = app.update(Message::StartSemanticReview(Id::from_u128(100)));
if mode == 2 {
// A paired attachment is a real catalog facet only after explicit
// correspondence; selecting an unstaged synthetic pair is rejected.
for (id, oa) in [(103, true), (104, false)] {
let choice = crate::app::Choice {
id: Id::from_u128(id),
label: format!("Record · @object_{id}"),
};
send_review(
&mut app,
if oa {
RowEvent::ReferenceOa(choice)
} else {
RowEvent::ReferenceSa(choice)
},
);
}
send_review(&mut app, RowEvent::AddReference);
}
send_review(&mut app, RowEvent::Select(facet.clone()));
send_review(
&mut app,
@@ -436,7 +455,19 @@ fn review_reference_facets_groups_and_attachment_hosts_browse_without_replanning
} else {
Id::from_u128(101)
};
assert_eq!(app.reference_draft(&host, path).unwrap().original, record);
assert_eq!(
app.reference_draft(&host, path)
.unwrap_or_else(|| {
panic!(
"Reference fixture mode={mode}, facet={facet:?}, selected={:?}, decisions={:?}, error={:?}",
review(&app).selected,
review(&app).decisions,
review(&app).error
)
})
.original,
record
);
let before = review(&app).recovery_snapshot().unwrap();
let plan = serde_json::to_value(&review(&app).plan).unwrap();
let proposed = review(&app).proposed.clone();
@@ -760,22 +760,34 @@ async fn mounted_reverse_review_required_rationale_remains_editable_beside_candi
let sa = p
.elements
.values()
.find(|element| {
element.layer() == Layer::Sa
&& matches!(
element.shared(),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
operation: Some(_),
message_kind: MessageKind::Call | MessageKind::Asynchronous,
..
},
..
})
)
})
.find(|element| element.alias == "operation_probe_43")
.unwrap()
.id;
let allocation = p
.relations
.values()
.find(|relation| relation.alias == "operation_probe_30")
.unwrap()
.id;
// The boundary fixture deliberately has no selected operation. Author one
// through the validated command path before testing reverse omission.
let mut message = p.elements[&sa].clone();
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { operation, .. },
..
}) = message.shared_mut()
else {
panic!("The operation fixture must contain an Exchange call")
};
*operation = Some(OperationUse {
allocation: Some(allocation),
});
p = execute_batch(&p, &[Command::EditElement(sa, message)]).unwrap();
assert!(matches!(
oa_core::operations::resolve(&p, sa).unwrap(),
oa_core::operations::OperationResolution::Selected(value)
if value.allocation == allocation
));
let inspector_id = Id::new_v4();
element(
&mut p,
@@ -836,7 +848,14 @@ async fn mounted_reverse_review_required_rationale_remains_editable_beside_candi
let candidate = serde_json::to_value(
review(&app)
.reference_form(app.project())
.unwrap()
.unwrap_or_else(|| {
panic!(
"Reverse review candidate: selected={:?}, decisions={:?}, error={:?}",
review(&app).selected,
review(&app).decisions,
review(&app).error
)
})
.recovery_snapshot(),
)
.unwrap();
+1
View File
@@ -17,6 +17,7 @@ use std::collections::BTreeMap;
#[cfg(test)]
mod contextual_source_tests;
pub(crate) mod creation;
mod form_session;
pub(crate) mod scenario;
// Projection acceptance precedes the coordinated scenario renderer integration.
#[cfg(test)]
@@ -0,0 +1,148 @@
//! Keep behavior field state within one editing session and reset it on replacement.
use iced::advanced::widget::{Operation, Tree, tree};
use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, overlay, renderer};
use iced::{Element, Event, Length, Rectangle, Size, Theme, Vector, mouse};
use oa_core::Id;
struct Identity(Id);
struct Session<'a, Message, R: renderer::Renderer> {
identity: Id,
content: Element<'a, Message, Theme, R>,
}
pub(super) fn reset_on_change<'a, Message: 'a, R: renderer::Renderer + 'a>(
identity: Id,
content: impl Into<Element<'a, Message, Theme, R>>,
) -> Element<'a, Message, Theme, R> {
Element::new(Session {
identity,
content: content.into(),
})
}
impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Session<'_, Message, R> {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<Identity>()
}
fn state(&self) -> tree::State {
tree::State::new(Identity(self.identity))
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
let retained = tree.state.downcast_mut::<Identity>();
if retained.0 != self.identity {
retained.0 = self.identity;
// Iced's keyed column preserves same-length child replacements.
// An editing-session replacement must instead get fresh field state.
tree.children = self.children();
} else {
tree.diff_children(std::slice::from_ref(&self.content));
}
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
operation: &mut dyn Operation,
) {
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &R,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &R,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &R,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, R>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
@@ -232,25 +232,31 @@ impl Operation for Collect {
};
let mut shifts = BTreeMap::new();
for ancestor in ancestors.iter().rev() {
let dx = reveal(
bounds.x,
bounds.width,
ancestor.bounds.x,
ancestor.bounds.width,
);
let dy = reveal(
bounds.y,
bounds.height,
ancestor.bounds.y,
ancestor.bounds.height,
);
// Drawing offsets are rounded; an already-visible axis must not
// acquire a scroll delta from a fractional capacity alone.
let delta = Vector::new(
(ancestor.offset.x
+ reveal(
bounds.x,
bounds.width,
ancestor.bounds.x,
ancestor.bounds.width,
))
.clamp(0., ancestor.capacity.x)
- ancestor.offset.x,
(ancestor.offset.y
+ reveal(
bounds.y,
bounds.height,
ancestor.bounds.y,
ancestor.bounds.height,
))
.clamp(0., ancestor.capacity.y)
- ancestor.offset.y,
if dx == 0. {
0.
} else {
(ancestor.offset.x + dx).clamp(0., ancestor.capacity.x) - ancestor.offset.x
},
if dy == 0. {
0.
} else {
(ancestor.offset.y + dy).clamp(0., ancestor.capacity.y) - ancestor.offset.y
},
);
if delta != Vector::ZERO {
shifts.insert(ancestor.index, delta);
@@ -1,7 +1,7 @@
//! Scenario presentation. Layout/camera controls never construct model commands.
use super::super::scenario_scroll;
use super::super::{form_session, scenario_scroll};
use super::*;
use iced::widget::{Space, column, keyed_column, sensor};
use iced::widget::{Space, column, sensor};
pub(crate) const NAME_INPUT: &str = "scenario-form-name";
@@ -754,16 +754,13 @@ fn editor_panel<'a, R: iced::advanced::text::Renderer + 'a>(
container(
column![
container(actions).id("scenario-actions").width(Fill),
keyed_column([(
form_session::reset_on_change(
session,
scrollable(fields)
.id("scenario-form")
.width(Fill)
.height(Fill)
.into(),
)])
.width(Fill)
.height(Fill),
.height(Fill),
),
]
.spacing(6),
)
@@ -183,9 +183,19 @@ impl Operation for Collect {
ancestor.bounds.y,
ancestor.bounds.height,
);
// Iced reports rounded drawing offsets, while capacity can be
// fractional. Clamping an already-visible axis can move it a pixel.
let delta = Vector::new(
(ancestor.offset.x + dx).clamp(0., ancestor.capacity.x) - ancestor.offset.x,
(ancestor.offset.y + dy).clamp(0., ancestor.capacity.y) - ancestor.offset.y,
if dx == 0. {
0.
} else {
(ancestor.offset.x + dx).clamp(0., ancestor.capacity.x) - ancestor.offset.x
},
if dy == 0. {
0.
} else {
(ancestor.offset.y + dy).clamp(0., ancestor.capacity.y) - ancestor.offset.y
},
);
if delta != Vector::ZERO {
shifts.insert(ancestor.index, delta);
@@ -0,0 +1,58 @@
# Chooser and editing-session corrections — Windows gate
Codex-956atf `/root`, 2026-09-11. The corrected source passes **chooser14/14** and
**scenario-workspace18/18**, including all four previously failing cases and all
five parent-focus I1/I2 regressions. No failed or ignored tests. The full720 rerun
is active; its terminal result remains a separate gate.
The [first full run](release-scenario-focus-gate-20260911.md#subsequent-full-suite-failure-and-corrective-work)
finished716 passed/4 failed. Its evidence is preserved. The
[chooser diagnosis](release-chooser-full-gate-diagnosis-20260911.md) records why
an already-visible anchor drifted by1px and why the rationale/attachment fixtures
stopped before their intended interaction. Exact assertions were retained.
Root replaced the one-child keyed-column assumption with a small generic
`behavior/form_session.rs` widget. It forwards layout/input/drawing/operations,
retains child state for the same editing identity, and creates a fresh child tree
when a different session replaces it without an intervening closed render. The
existing test now verifies zero new-form scroll while preserving the canvas's
actual137×850 offset, same-session700px scroll, raw recovery and source/history.
The helper is shared with the future isolated process-editor union.
The chooser and parent focus collectors now preserve zero requested reveal on
each axis. Rounded drawing offsets no longer create a spurious delta from a
fractional scroll capacity. The exact pointer-versus-keyboard anchor assertion
now passes. The corrected operation fixture explicitly selects and resolves its
actual interface operation; the paired-attachment fixture establishes real child
correspondence through public events before selecting its facet. Neither change
relaxes production ownership/write guards.
## Actual commands and evidence
Root used Rust/Cargo1.98.1 Windows GNU/LLVM, one Cargo job and ordinary test-stack
settings. Standalone scoped rustfmt and `git diff --check` pass.
| Command | Result |
| --- | --- |
| `cargo test --locked -p oa-desktop --bin arcadia-studio app::reference_authoring::tests:: -- --test-threads=1` | Exit0; warning-free compile2m01s;14 passed,706 filtered;64.11s. |
| Same prefix with `behavior::scenario::workspace::tests::` | Exit0;18 passed,702 filtered;4.89s. |
Immutable `.local/chooser-correction-gate-20260911/` contains the corrected
executable, verified runtime DLL and exact focused logs. Its512-input manifest
includes Rust/Cargo inputs plus shared Windows build resources, docs, examples
and harness fixtures. The earlier275-input manifest remains a narrower preserved
source inventory for the earlier executable, not this corrected gate.
| Artifact | SHA256 |
| --- | --- |
| 135,405,568-byte corrected test executable | `05811E18E4FF3495EECFD585947234906F27F7709B7E583F0362477EF1A943C3` |
| 190,464-byte libunwind.dll | `AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A` |
| Chooser14 log | `EFA48AE5C66C8D839635F61DE2F9C798DF809FA686B3F173FE112B28558AAAF4` |
| Scenario18 log | `10B7EF829A7AF0D741571701EC257D88B71A0D5EF7BB53A2022AE000B5FF1CEE` |
| Working-input/artifact manifest | `985C06A3B9926C9096F97D59DDE57CE3494F2FE06008BB9F46FBD5578126B4BB` |
Independent follow-up review of root's session wrapper and parent reveal
correction is active. Full720/current desktop Clippy and matching native Windows
GUI focus/visual acceptance remain open. The isolated shared canvas and schema8
codec changes are outside this build. Further Linux builds/checks are deferred
by the user's15:28 UTC instruction until the user reports networking resolved.
@@ -0,0 +1,53 @@
# Chooser full-gate diagnosis — 2026-09-11
Codex-956atf `/root/chooser_gate_debug`. Initial read-only source diagnosis, followed by root's narrow three-file correction lease after the suite reached terminal failure. No compiler, saved-executable, native, WSL, network/VPN, or Git actions. Root remains sole execution/integration owner. Read AGENTS.md, current issue 1 comments through 346, empty open PR inventory, ScenarioFocus20260911 Wiki and prior chooser production review. This report supersedes no historical result.
## Actual evidence
Root identifies source `8895dd2e0f87c0f16085bf4b478b8c0fbed38685` and immutable `.local/scenario-focus-gate-20260911/arcadia_studio-e358a5ed59b1de41.exe`, SHA256 `C9C4210EA36854F20C0DC9F38137CEFE5C3BC9CD19E10E1AD41DCB410CC92D04`. Reviewer independently read the terminal `.local/logs/scenario-focus-full720-20260911.log`: **716 passed, 4 failed, 0 ignored**, 377.91 seconds. Root records log SHA256 `4A765FFAD4B08EC0B9D6B8399CF841E3C776051EA07B4433BB47C8E6BDC4C67F`. Reviewer did not execute it. Three assigned failures are below; scenario fresh-session scroll reset is root-owned and outside this diagnosis.
Paths below are relative to `crates/oa-desktop/src` unless otherwise specified.
## Rationale fixture stops before any widget interaction
Actual failure: `app/reference_authoring/tests/mounted.rs:777`, Option unwrap while finding an SA Exchange occurrence whose operation is `Some`. No rationale field is mounted, clicked, or typed before this failure.
The loaded `harness/fixtures/interface-operation-probe/output/boundary-only.json:188–228` has calls `operation_probe_43` and `operation_probe_44` with no authored operation field. The fixture represents an unselected operation. Existing `semantic_review/source7_tests.rs:285–301` shows the established explicit setup: select call 43, set `OperationUse { allocation: Some(operation_probe_30) }`, then validate via `execute_batch`.
Minimal correction suggestion: prepare an explicitly operation-bearing call using that established setup before the search/reverse-review construction, and assert successful actual operation resolution. Do not merely broaden the search to accept `None`: that would no longer establish the operation-omission premise. Preserve every per-key addressed rationale event, final `Engineering reason`, retained focus, candidate recovery, Inspector raw, SourceBundle and Undo assertion.
Production path to verify after setup: `reference_authoring.rs::reference_overlay` selects modal form ownership; `forms/reference_chooser/widget.rs:282–284` calls `trigger::own_input`; `trigger/focus.rs::exclusive` uses the whole `reference-owning-review-modal` boundary; `semantic_review/row.rs:182` receives `OperationRationale`. The current source boundary preserves sibling inputs; this failing execution neither proves nor disproves that behavior.
## Keyboard return moves an already-visible anchor by one pixel
Actual failure: `app/reference_authoring/tests/mounted.rs:463`, after pointer Open and Escape return, keyboard Open anchor is `Rectangle { x: 0.0, y: 124.79999, width: 300.0, height: 16.0 }`; original expected is identical except `y: 123.79999`. The preparatory actual-scroll assertion, pointer message assertion and first exact pointer anchor equality already passed.
Concrete source defect: `forms/reference_chooser/trigger/focus.rs:185–188` computes `(ancestor.offset + reveal_delta).clamp(0, capacity) - ancestor.offset` even when reveal_delta is zero. The installed Iced 0.14.2 `scrollable.rs:549–559` passes the rounded drawing translation to the operation; `scrollable.rs:1876–1901` rounds each axis. Capacity derives from fractional content minus viewport size. A rounded END offset can exceed that fractional capacity; clamping then creates a negative delta for an already-visible field. Iced `State::scroll` adds that delta to the unrounded underlying absolute offset (`scrollable.rs:1790–1802`), potentially crossing the adjacent pixel boundary. This predicts precisely the observed +1px anchor.
Minimal correction suggestion: zero requested reveal on an axis must produce exactly zero applied delta on that axis; only bound an axis that actually needs revealing. Preserve the exact rectangle equality; adding a tolerance would hide an unintended scroll. Keep existing fresh entry, nested reveal, return-after-scrolling-away and ownership tests.
Needed falsifier: root narrow reproduction logging field bounds, ancestor rounded offset, fractional capacity, dx/dy and applied delta immediately before return. Expected defect signature is dx=dy=0 with a negative nonzero clamp delta at END. After correction, return should leave the scroll unchanged and both pointer/keyboard anchors equal. No runtime instrumentation has yet been applied.
## Paired attachment facet may not be legitimately staged
Actual failure: `app/reference_authoring/tests.rs:439`, first `reference_draft(host,path).unwrap()` in the loop over scalar, constructor group and child attachment. The log lacks the loop mode, active selected facet and errors. No chooser Open occurs in the failing iteration.
Strong source hypothesis: mode 2 constructs `Child { oa: 103, sa: 104 }` and sends Select without staging this pair or establishing 103→104 correspondence. `oa-core/src/semantic/catalog.rs:690–703` derives child pairs from the reference map. Without that mapping, the current catalog has separate unpaired child selectors. Batch Select still invokes refresh (`semantic_review/batch.rs:395–400,584–586`); `semantic_review/row.rs:863–875` replaces selection if its selector is absent from current facts. Later Decide stores the explicit Candidate facet and attachment draft, but does not select it. `row.rs:529–545` correctly rejects a host facet that is not actively selected.
The scalar banked-row reference test passed in the same executable, and `Document::from_project` explicitly proves model equality after source roundtrip, so source normalization loss is unsupported here. The paired-attachment diagnosis still requires actual mode/selection evidence.
Needed falsifier: enrich the failing assertion with mode, requested facet, current selected facet, decisions, state error, candidate roots and candidate draft availability. If mode2/selection mismatch is confirmed, prepare its pair through the actual Attachment Pick/Stage workflow before Select (or establish explicit correspondence through its real workflow), retaining the old-facet rejection and source/history comparisons. Do not relax the production active-facet guard to accept a banked/unselected draft. If another mode fails, re-localize against its actual gate before changing source.
## Handoff
Root granted the narrow lease after reviewing the actual failure evidence. Three files were edited and standalone rustfmt completed exit 0 with edition2024 and skip_children=true:
| File | Frozen correction SHA256 |
| --- | --- |
| `forms/reference_chooser/trigger/focus.rs` | `F1E79F4B03B2F9FD1CDC335054D4AE177DC9E759B6EA3D334B2BCCA39B57191B` |
| `app/reference_authoring/tests/mounted.rs` | `3A43E32BD028D4DF699F905BDBFAA7B99A56D5ADB5EB190AB6E47E14F6D7520C` |
| `app/reference_authoring/tests.rs` | `AAE2D95305DBCA6120DCFBFBF292F138C39015F136F3C85784CA46B0669729B8` |
The production correction guards each zero reveal axis before the capacity clamp. The rationale fixture follows the existing validated EditElement operation-selection setup and additionally proves actual Selected resolution. Mode2 uses the real public ReferenceOa, ReferenceSa and AddReference events to establish103→104 correspondence before selecting its paired Child catalog facet; semantic-review visibility and write guards are unchanged. Candidate fixture failures now include mode/facet/selected/decisions/error diagnostics. Exact anchor, key counts, final rationale, stale-facet rejection, candidate staging, SourceBundle and history expectations were retained.
These are prepared source corrections, not passing execution evidence. Root owns targeted chooser and full-suite reruns and integration with its separate session-scroll correction. Existing scenario parent-focus5/5 and independent canvas/schema8 work remain separate.
@@ -41,6 +41,11 @@ manifest of actual source/build files, including the new untracked focus module.
The manifest explicitly identifies working-source provenance rather than claiming
all inputs were identical to base HEAD.
The correction is now committed as
`8895dd2e0f87c0f16085bf4b478b8c0fbed38685`, pushed and exact readback verified.
Root independently compared all275 manifest inputs with that commit's Git archive;
there were zero mismatches. `commit-readback.json` preserves the comparison.
| Artifact | SHA256 |
| --- | --- |
| 135,520,256-byte test executable | `C9C4210EA36854F20C0DC9F38137CEFE5C3BC9CD19E10E1AD41DCB410CC92D04` |
@@ -54,3 +59,26 @@ focus timing/visual behavior and Linux desktop acceptance remain separate gates.
The isolated shared canvas candidate is not part of this binary; its future union
must preserve the exact parent marker, session/mount split, Name identity, sensor
and all five regressions. Prior676-suite/native80cf197 evidence remains historical.
## Subsequent full-suite failure and corrective work
Root ran the preserved executable with `--test-threads=1` and captured the complete
720-test run: exit101,716 passed/4 failed, no ignored tests,377.91s. All five parent
focus tests pass within this run. The saved full log SHA256 is
`4A765FFAD4B08EC0B9D6B8399CF841E3C776051EA07B4433BB47C8E6BDC4C67F`.
Three failures concern chooser fixtures/scrolling; the separately
[recorded diagnosis](release-chooser-full-gate-diagnosis-20260911.md) preserves
exact panic lines and values. The fourth is the existing
`new_scenario_form_resets_only_its_scroll_and_recovery_gets_a_fresh_session`:
after Cancel→Start without a closed render, the new form retained700px instead
of zero. Installed Iced0.14.2 keyed-column diff does not replace same-length child
state merely because its key changed. Root introduced a small explicit editing
session wrapper which replaces only its child tree on identity change and retains
same-session field state. The original complete canvas/source/history assertions
are unchanged. The wrapper is reusable for the isolated process editor's invocation.
Root also applies the chooser's zero-reveal-axis correction to the parent focus
collector's identical fractional-capacity calculation. Corrected source is not
yet covered by this saved binary; a new focused/full gate is required. No fixture
tolerance, disabled test, stack override or renderer skip was introduced.