Fix inline architecture naming and captured Escape cancellation
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-09-10 11:10:24 -07:00
parent 1797516f46
commit 7d1df4f369
9 changed files with 722 additions and 21 deletions
+2 -3
View File
@@ -1019,6 +1019,7 @@ impl App {
iced::time::every(std::time::Duration::from_secs(10)).map(|_| Message::CheckpointTick),
window::close_requests().map(|id| Message::Request(Pending::Close(id))),
window::resize_events().map(|(_, size)| Message::WindowResized(size)),
architecture_authoring::subscription(self),
iced::event::listen_with(|event, status, _| {
let iced::Event::Keyboard(keyboard::Event::KeyPressed {
key,
@@ -1076,9 +1077,7 @@ impl App {
// navigation and close retain raw input until explicit completion/cancel.
if self.architecture.is_some() {
if matches!(message, Message::Escape) {
self.architecture = None;
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
return Task::none();
return self.architecture_event(architecture_authoring::Event::Cancel);
}
let placing = self.architecture.as_ref().is_some_and(|s| s.saved.placing);
if !matches!(
@@ -3,6 +3,8 @@ use super::*;
use iced::widget::{button, column, pick_list, row, scrollable, text, text_input};
use serde::{Deserialize, Serialize};
mod palette;
#[cfg(test)]
mod tests;
pub use palette::view as palette;
#[derive(Debug, Clone)]
@@ -21,9 +23,35 @@ pub enum Event {
Filter(Filter),
ShowPlaced(bool),
Page(usize),
Edit,
CapturedEscape,
Cancel,
}
pub fn subscription(app: &App) -> Subscription<Message> {
if app.architecture.as_ref().is_some_and(State::inline) {
iced::event::listen_with(captured_escape)
} else {
Subscription::none()
}
}
fn captured_escape(
event: iced::Event,
status: iced::event::Status,
_: iced::window::Id,
) -> Option<Message> {
// A focused Iced text input captures Escape while dropping its focus. Only
// inline architecture naming opts into also cancelling that same keypress.
(status == iced::event::Status::Captured
&& matches!(
event,
iced::Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape),
..
})
))
.then_some(Message::Architecture(Event::CapturedEscape))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Filter {
@@ -336,14 +364,20 @@ impl App {
}
return iced::widget::operation::focus("architecture-name");
}
Event::CapturedEscape => {
if self.architecture.as_ref().is_some_and(State::inline) {
return self.architecture_event(Event::Cancel);
}
}
Event::Cancel => {
self.architecture = None;
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
self.status = "Cancelled · Select / move".into();
}
Event::Name(value) => {
if value.len() <= 16384
&& let Some(s) = &mut self.architecture
&& let (Intent::Create { name, .. } | Intent::Rename { name, .. }) =
&& let Intent::Create { name, .. } | Intent::Rename { name, .. } =
&mut s.saved.intent
{
*name = value;
@@ -375,13 +409,6 @@ impl App {
s.saved.page = page.min(1_000_000);
}
}
Event::Edit => {
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
if let Some(s) = &mut self.architecture {
s.saved.placing = false;
}
return iced::widget::operation::focus("architecture-name");
}
Event::Pick(id) => {
if !candidates(self)
.iter()
@@ -422,7 +449,10 @@ impl App {
}
}
Event::Click(context, point) => {
if self.route_context() != Some(context) {
if self.route_context() != Some(context)
|| !point.x.is_finite()
|| !point.y.is_finite()
{
return Task::none();
}
if self.architecture.as_ref().is_some_and(|s| s.saved.placing) {
@@ -460,6 +490,10 @@ impl App {
self.status =
"The diagram changed. The name is retained; cancel and restart this placement."
.into();
if let Some(s) = &mut self.architecture {
s.saved.placing = false;
s.error = Some(self.status.clone());
}
return;
}
let diagram = s.saved.diagram;
@@ -553,9 +587,10 @@ pub fn inline_editor(app: &App) -> Option<Element<'_, Message>> {
row![
iced::widget::Space::new().width(x),
iced::widget::opaque(
iced::widget::container(name_editor(app))
iced::widget::container(scrollable(name_editor(app)))
.padding(10)
.width(width)
.max_height((size.height - y - 8.).max(0.))
.style(crate::style::card)
)
]
@@ -165,7 +165,7 @@ fn tool<'a>(
.into()
}
pub fn view(app: &App) -> Element<'_, Message> {
let mut content = column![tool(
let mut content = iced::widget::column![tool(
app,
Icon::Collapse(app.architecture_palette_expanded),
if app.architecture_palette_expanded {
@@ -0,0 +1,378 @@
use super::*;
fn setup(layer: Layer) -> App {
let mut app = App::with_project(Project::empty());
app.layer = layer;
app.active_diagram = None;
let _ = app.update(Message::AddDiagram);
app.zoom = 0.75;
app.pan = iced::Vector::new(37., -19.);
app
}
fn send(app: &mut App, event: Event) {
let _ = app.update(Message::Architecture(event));
}
fn create(app: &mut App, kind: Kind, name: &str) -> Id {
send(app, Event::Start(kind, Some(Position { x: 120., y: 160. })));
send(app, Event::Name(name.into()));
send(app, Event::Submit);
assert!(app.architecture.is_none(), "{}", app.status);
app.selected
}
#[test]
fn architecture_allocated_work_is_root_owned_and_one_exact_history_change() {
for (layer, participant, work) in [
(Layer::Oa, Kind::Entity, Kind::Activity),
(Layer::Sa, Kind::System, Kind::Function),
] {
let mut app = setup(layer);
let performer = create(&mut app, participant, "Control team");
let before = app.document.bundle().clone();
let camera = (app.zoom, app.pan);
let point = Position { x: 151., y: 221. };
send(&mut app, Event::Allocate(work, performer, Some(point)));
send(&mut app, Event::Name(" Report 雨 ".into()));
assert_eq!(app.document.bundle(), &before);
send(&mut app, Event::Submit);
assert!(app.architecture.is_none(), "{}", app.status);
let created = app.selected;
let element = &app.project().elements[&created];
assert_eq!(element.kind(), work);
assert_eq!(element.name, " Report 雨 ");
assert_eq!(element.parent, None);
assert!(app.project().relations.values().any(|r| {
r.kind() == RelationKind::Allocation && r.source() == performer && r.target() == created
}));
let d = &app.project().diagrams[&app.active_diagram.unwrap()];
assert_eq!(d.nodes[&app.selected_node.unwrap()].position, point);
assert_eq!((app.zoom, app.pan), camera);
let accepted = app.document.bundle().clone();
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before);
let _ = app.update(Message::Redo);
assert_eq!(app.document.bundle(), &accepted);
}
}
#[test]
fn architecture_f2_routes_selected_node_to_inline_rename_and_exact_undo() {
use iced::widget::canvas::{Event as CanvasEvent, Program};
use iced::{keyboard, mouse};
let mut app = setup(Layer::Oa);
let element = create(&mut app, Kind::Entity, "Original team");
let before = app.document.bundle().clone();
let diagrams = app.project().diagrams.clone();
let camera = (app.zoom, app.pan);
let bounds = iced::Rectangle::new(iced::Point::ORIGIN, iced::Size::new(600., 380.));
let canvas = crate::diagram::DiagramCanvas {
project: app.project(),
view_index: None,
diagram: app.active_diagram.unwrap(),
selected: element,
zoom: app.zoom,
pan: app.pan,
context: app.route_context().unwrap(),
selected_bend: None,
editable: true,
placing: false,
connection: None,
};
let mut state = crate::diagram::State::default();
let key = CanvasEvent::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::F2),
modified_key: keyboard::Key::Named(keyboard::key::Named::F2),
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::F2),
location: keyboard::Location::Standard,
modifiers: keyboard::Modifiers::empty(),
text: None,
repeat: false,
});
assert!(
canvas
.update(&mut state, &key, bounds, mouse::Cursor::Unavailable)
.is_none()
);
let _ = canvas.update(
&mut state,
&CanvasEvent::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
bounds,
mouse::Cursor::Available(iced::Point::new(150., 120.)),
);
let action = canvas
.update(&mut state, &key, bounds, mouse::Cursor::Unavailable)
.unwrap();
let message = action.into_inner().0.unwrap();
let _ = app.update(message);
assert!(app.architecture.as_ref().unwrap().inline());
assert!(matches!(&app.architecture.as_ref().unwrap().saved.intent,
Intent::Rename { element: id, name } if *id == element && name == "Original team"));
send(&mut app, Event::Name(" Renamed team 雨 ".into()));
assert_eq!(app.document.bundle(), &before);
send(&mut app, Event::Submit);
assert!(app.architecture.is_none(), "{}", app.status);
assert_eq!(app.project().elements[&element].name, " Renamed team 雨 ");
assert_eq!(app.project().diagrams, diagrams);
assert_eq!((app.zoom, app.pan), camera);
let accepted = app.document.bundle().clone();
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before);
let _ = app.update(Message::Redo);
assert_eq!(app.document.bundle(), &accepted);
}
#[test]
fn architecture_blank_names_and_escape_never_accept_placeholder_or_rename() {
let mut app = setup(Layer::Oa);
let element = create(&mut app, Kind::Actor, "Dispatcher");
let before = app.document.bundle().clone();
let context = app.route_context().unwrap();
for event in [
Event::Start(Kind::Activity, Some(Position { x: 220., y: 330. })),
Event::Rename(context, element, Position { x: 150., y: 200. }),
] {
// Every cancel changes the gesture context; use the current context for F2.
let event = match event {
Event::Rename(_, element, point) => {
Event::Rename(app.route_context().unwrap(), element, point)
}
event => event,
};
send(&mut app, event);
send(&mut app, Event::Name(" \t ".into()));
send(&mut app, Event::Submit);
assert!(app.architecture.as_ref().unwrap().inline());
assert!(app.architecture.as_ref().unwrap().error.is_some());
assert_eq!(app.document.bundle(), &before);
let _ = app.update(Message::Escape);
assert!(app.architecture.is_none());
assert_eq!(app.document.bundle(), &before);
}
}
#[test]
fn architecture_invalid_click_cannot_poison_inline_or_checkpoint_geometry() {
let mut app = setup(Layer::Oa);
send(&mut app, Event::Start(Kind::Entity, None));
let before = app.document.bundle().clone();
let context = app.route_context().unwrap();
for point in [
Position { x: f32::NAN, y: 0. },
Position {
x: 0.,
y: f32::INFINITY,
},
] {
send(&mut app, Event::Click(context, point));
let draft = app.architecture.as_ref().unwrap();
assert!(draft.unnamed_tool());
assert!(validate_saved(app.project(), &draft.saved).is_ok());
assert_eq!(app.document.bundle(), &before);
}
send(&mut app, Event::Click(context, Position { x: 25., y: 30. }));
assert!(app.architecture.as_ref().unwrap().inline());
}
#[test]
fn architecture_stale_submit_keeps_create_and_rename_visible_and_exact() {
for rename in [false, true] {
for stale_document in [false, true] {
let mut app = setup(Layer::Oa);
let element = create(&mut app, Kind::Entity, "Accepted team");
let point = Position {
x: 151.5,
y: 227.25,
};
let event = if rename {
Event::Rename(app.route_context().unwrap(), element, point)
} else {
Event::Start(Kind::Activity, Some(point))
};
send(&mut app, event);
send(
&mut app,
Event::Name(" Retained engineering name 雨 ".into()),
);
// Change only the binding. The accepted model/source must not be
// rewritten or a rejected draft silently rebound to the new context.
if stale_document {
app.architecture.as_mut().unwrap().document = Id::new_v4();
} else {
app.architecture.as_mut().unwrap().revision = app.document.revision() + 1;
}
let draft = app.architecture.as_ref().unwrap();
let saved = serde_json::to_value(&draft.saved).unwrap();
let binding = (draft.document, draft.revision);
let model = app.project().clone();
let source = app.document.bundle().clone();
let revision = app.document.revision();
let camera = (app.zoom, app.pan);
for _ in 0..2 {
send(&mut app, Event::Submit);
let draft = app.architecture.as_ref().unwrap();
assert!(
draft.inline(),
"rename={rename}, stale_document={stale_document}"
);
assert!(inline_editor(&app).is_some());
assert_eq!(draft.error.as_deref(), Some(app.status.as_str()));
assert!(app.status.contains("cancel and restart"));
assert_eq!(serde_json::to_value(&draft.saved).unwrap(), saved);
assert_eq!((draft.document, draft.revision), binding);
assert_eq!(app.project(), &model);
assert_eq!(app.document.bundle(), &source);
assert_eq!(app.document.revision(), revision);
assert_eq!((app.zoom, app.pan), camera);
}
let _ = app.update(Message::Escape);
assert!(app.architecture.is_none());
assert_eq!(app.project(), &model);
assert_eq!(app.document.bundle(), &source);
let fresh = create(&mut app, Kind::Actor, "Fresh context");
assert_eq!(app.project().elements[&fresh].name, "Fresh context");
let _ = app.update(Message::Undo);
assert_eq!(app.project(), &model);
assert_eq!(app.document.bundle(), &source);
}
}
}
fn key_event(named: iced::keyboard::key::Named) -> iced::Event {
use iced::keyboard;
iced::Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(named),
modified_key: keyboard::Key::Named(named),
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Escape),
location: keyboard::Location::Standard,
modifiers: keyboard::Modifiers::empty(),
text: None,
repeat: false,
})
}
fn focused_text_input_escape(name: &str) -> Message {
use iced::advanced::{Layout, Shell, layout, widget};
let mut field: iced::Element<'_, (), iced::Theme, ()> = text_input("Engineering name", name)
.id("architecture-name")
.on_input(|_| ())
.into();
let mut tree = widget::Tree::new(field.as_widget());
let size = iced::Size::new(300., 40.);
let node =
field
.as_widget_mut()
.layout(&mut tree, &(), &layout::Limits::new(iced::Size::ZERO, size));
tree.state
.downcast_mut::<iced::widget::text_input::State<()>>()
.focus();
let escape = key_event(iced::keyboard::key::Named::Escape);
let mut messages = Vec::new();
let mut shell = Shell::new(&mut messages);
field.as_widget_mut().update(
&mut tree,
&escape,
Layout::new(&node),
iced::mouse::Cursor::Unavailable,
&(),
&mut iced::advanced::clipboard::Null,
&mut shell,
&iced::Rectangle::new(iced::Point::ORIGIN, size),
);
assert!(
shell.is_event_captured(),
"Iced consumes focused-input Escape"
);
assert!(
!tree
.state
.downcast_ref::<iced::widget::text_input::State<()>>()
.is_focused()
);
captured_escape(
escape,
iced::event::Status::Captured,
iced::window::Id::unique(),
)
.unwrap()
}
#[test]
fn architecture_first_focused_input_escape_cancels_create_and_rename() {
for rename in [false, true] {
let mut app = setup(Layer::Oa);
let element = create(&mut app, Kind::Actor, "Accepted dispatcher");
let model = app.project().clone();
let source = app.document.bundle().clone();
let revision = app.document.revision();
let camera = (app.zoom, app.pan);
let point = Position { x: 439., y: 328.2 };
let event = if rename {
Event::Rename(app.route_context().unwrap(), element, point)
} else {
Event::Start(Kind::Actor, Some(point))
};
send(&mut app, event);
let name = " Unaccepted long engineering name 雨 ";
send(&mut app, Event::Name(name.into()));
assert!(app.architecture.as_ref().unwrap().inline());
let message = focused_text_input_escape(name);
let _ = app.update(message);
assert!(app.architecture.is_none());
assert!(inline_editor(&app).is_none());
assert_eq!(app.status, "Cancelled · Select / move");
assert_eq!(app.project(), &model);
assert_eq!(app.document.bundle(), &source);
assert_eq!(app.document.revision(), revision);
assert_eq!((app.zoom, app.pan), camera);
}
}
#[test]
fn architecture_captured_escape_does_not_cancel_other_controls_or_placement() {
let mut app = setup(Layer::Oa);
for event in [
None,
Some(Event::Start(Kind::Entity, None)),
Some(Event::Existing(None)),
] {
send(&mut app, Event::Cancel);
if let Some(event) = event {
send(&mut app, event);
}
let saved = app
.architecture
.as_ref()
.map(|s| serde_json::to_value(&s.saved).unwrap());
let before = app.document.bundle().clone();
let status = app.status.clone();
let epoch = app.canvas_epoch;
let message = focused_text_input_escape("Other field or existing-element search");
let _ = app.update(message);
assert_eq!(
app.architecture
.as_ref()
.map(|s| serde_json::to_value(&s.saved).unwrap()),
saved
);
assert_eq!(app.document.bundle(), &before);
assert_eq!(app.status, status);
assert_eq!(app.canvas_epoch, epoch);
}
assert!(
captured_escape(
key_event(iced::keyboard::key::Named::Escape),
iced::event::Status::Ignored,
iced::window::Id::unique(),
)
.is_none()
);
assert!(
captured_escape(
key_event(iced::keyboard::key::Named::Enter),
iced::event::Status::Captured,
iced::window::Id::unique(),
)
.is_none()
);
}
+3 -2
View File
@@ -621,8 +621,9 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
Key::Named(Named::F2)
if self.editable && self.connection.is_none() && modifiers.is_empty() =>
{
let view = self.scene(state).view;
let node = view
let scene = self.scene(state);
let node = scene
.view
.nodes
.iter()
.find(|n| n.element == self.selected || n.id == self.selected)?;
+4 -4
View File
@@ -187,8 +187,8 @@ fn article(topic: Topic) -> Article {
&[
"Choose New in the top bar. Use Project properties to name the model and describe its purpose. Apply changes commits the property draft.",
"On Architecture, choose Create architecture view. Architecture · allocated work shows work inside its performer.",
"Choose Entity in the diagram's Create toolbar. Enter Rescue coordinator, choose Choose position, then click the diagram. Use Actor for an external participant such as a Vessel in distress. You can also right-click a blank area, choose Create Entity here, enter its name and choose Create here.",
"Choose Activity in the same toolbar, name it Assess incident and place it. Select Rescue coordinator and use + Relationship with Allocation to assign Assess incident to it. The allocated-work view then shows the activity inside its performer.",
"Expand the diagram's side palette to see tool names. Choose Entity, click its position in the diagram, enter Rescue coordinator and press Enter. Escape cancels the draft. Use Actor for an external participant such as a Vessel in distress. Right-clicking a blank area also offers Create Entity here.",
"Right-click Rescue coordinator and choose Add activity. Enter Assess incident and press Enter to create the activity and its performer allocation together. Ordinary Activity creation in the palette leaves work unallocated; use + Relationship with Allocation to assign existing work. The allocated-work view shows work inside its performer.",
"Use Add existing to place an element that is already in the model. The initial choices match this view; no search is required. Include already shown offers Locate for elements already placed here.",
"Add a second activity for another participant. Select an activity, choose + Relationship, select Interaction and choose the compatible target. Name the exchange after what is conveyed.",
"Visit Model checks to review unfinished work. Save all writes the accepted model and applied drafts to your chosen file.",
@@ -262,8 +262,8 @@ fn article(topic: Topic) -> Article {
"Create and allocate work",
&[
"Choose the Operational or System perspective. Create participants using Entity/Actor in OA or System/System actor in SA.",
"In the diagram's Create toolbar, choose Activity in OA or Function in SA, enter a name and click to place it. For semantic decomposition, use Model explorer, select existing work and use + In selected to create smaller work units.",
"To assign work to a participant, use + Relationship with Allocation from the participant to the leaf work. Model explorer also offers + Allocated when a participant and the appropriate work kind are selected.",
"In the diagram's side palette, choose Activity in OA or Function in SA, click its position, enter a name and press Enter. Escape cancels without creating an element. Select a card and press F2 to rename it in place. For semantic decomposition, use Model explorer, select existing work and use + In selected to create smaller work units.",
"Right-click a participant and choose Add activity or Add function to create new work and its allocation together. To assign existing leaf work, use + Relationship with Allocation from the participant to the work. Model explorer also offers + Allocated when a participant and the appropriate work kind are selected.",
"Choose Architecture · allocated work to review responsibilities. Choose Functional dataflow to review the activity/function hierarchy and its exchanges independently.",
],
),
@@ -0,0 +1,140 @@
# Compact palette implementation verification — 2026-09-10
Owner: Codex `/root/palette_finish`, assigned by root. Base branch
`codex/release1-continuation`, starting HEAD `1797516`. No commit or push is
performed by this worker. Root owns native QA, guide changes, integration and the
shared release ledgers.
Read repository AGENTS.md, Gitea issue #1/comments, Wiki Home, open PR inventory
(empty), and `harness/release-capella-basic-editing-20260907.md` before editing.
Ownership and results are recorded in issue #1.
## Changes
* Qualify the Iced `column!` macro in the palette to remove E0659.
* Borrow the projected scene when F2 finds its selected node instead of moving
its view out of `Arc`, removing E0507.
* Remove redundant or-pattern parentheses and the unused old `Edit` event.
* Reject non-finite placement coordinates before they can become inline-editor
or recovery-draft geometry. Existing context/epoch guards continue rejecting
cancelled or stale canvas gestures.
* Bound the inline name card to the remaining canvas height and make its content
scrollable, so a wrapped performer title or validation error can remain inside
the canvas. The card stays in the canvas stack and does not add a toolbar row.
Four new regression tests live in
`crates/oa-desktop/src/app/architecture_authoring/tests.rs`:
1. OA and SA contextual creation preserves root ownership while adding the
allocation and placement atomically; exact source bundle Undo/Redo and camera
values are checked.
2. A real Iced canvas F2 event requires canvas keyboard focus, dispatches the
selected node to inline rename, retains its exact engineering name, and
preserves placements/camera and exact source bundle Undo/Redo.
3. Blank creation/rename submission and Escape never accept a placeholder or
change the accepted source bundle.
4. NaN/infinite canvas coordinates cannot poison draft geometry or recovery
validation; the next valid click still opens naming.
## Verification
All Cargo commands use `.local/toolchain/env.ps1`,
`CARGO_BUILD_JOBS=1`, `--locked`, and the worker's exclusive Cargo lease.
The first repaired build passed 15 existing `architecture_` filtered tests,
with 569 filtered out. Log: `.local/palette-focused-initial-20260910.log`.
It exposed the unused `Edit` warning, subsequently removed. An initial new-test
compile caught an incorrect SourceBundle assertion; it was corrected before
the final tests.
The full current desktop suite passed **588 tests, 0 failures, 0 ignored**:
`cargo test --locked -p oa-desktop --bin arcadia-studio -- --test-threads=1`.
Compilation/link took 44.58 s and test execution 113.14 s. This includes the
four new regressions and existing placement/recovery/connection/transition
coverage. Log: `.local/palette-desktop-tests-20260910.log`.
`cargo build --locked -p oa-desktop --bin arcadia-studio` passed in 2m 03s,
including root's updated guide text. Log: `.local/palette-gui-build-20260910.log`.
`cargo fmt --all -- --check` passed; its empty success log is
`.local/palette-fmt-20260910.log`.
Built executable:
`C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\arcadia-studio.exe`
(109,516,288 bytes), modified `2026-09-10T17:42:00.6365121Z`.
SHA-256: `1B47C096624BB1101BEC8B18E63CD601A259DFF8C1BEC24581C31C6B511F9411`.
Root received this path/hash for an isolated native-QA staging copy.
`cargo clippy --locked -p oa-desktop --all-targets -- -D warnings` passed in
47.56 s. Log: `.local/palette-clippy-20260910.log`.
All assigned gates are complete. The worker released its Cargo lease and source
freeze to root; subsequent workers must obtain the next lease from root. Source
and report changes remain uncommitted for root's integration. No native UI was
operated by this worker.
## Scope and remaining acceptance
Automated application/canvas tests establish state transitions and exact history;
they do not establish native focus, visual compactness or pointer ergonomics.
Root must inspect the newly built executable at wide/minimum sizes, especially
bottom/right-edge naming, long names, wrapped errors, expanded palette labels,
Enter/Escape, F2, existing insertion/Locate and contextual allocation.
Canvas origin must remain stable through arming, naming, validation, commit and
cancel; only the explicit palette width toggle may resize the canvas. Continue
the independent basic-editing oracle, Save/reopen/recovery and OA/SA transition
destination checks. No Windows release acceptance or Linux runtime acceptance
is inferred from this implementation report.
## Follow-up: stale submission rejection
Independent local review and the separate Codex-956atf host's issue #1 comment
#12 identified a remaining stale-submit defect after the first verification:
Submit set `placing=true`, then a stale document/revision guard returned with
only a status string. That hid the retained inline name card. Earlier passing
gates above did not cover this rejected-submit path.
Codex-9e8r6t `/root/palette_finish` resumed the assigned architecture scope and
sole Cargo lease. Guard rejection now restores `placing=false` and copies the
explanation into the card's error, without rebinding its document/revision or
changing its name, point or intent.
A new regression covers Create/Rename × stale document/revision (four cases),
repeated Submit without rebinding, actual inline-view presence, exact serialized
saved draft, accepted Project/source/revision/camera preservation, Escape, and
successful fresh creation with exact Undo after cancellation. The focused
`architecture_` gate passed 20 tests, 569 filtered out, in 2.22 s after 48.63 s
compilation. Log: `.local/palette-stale-submit-tests-20260910.log`.
Root's native `1B47C096…` check then found that the first Escape was consumed by
the focused Iced name field and only unfocused it. The original global keyboard
subscription intentionally ignores captured Escape; that policy remains intact.
A separate subscription is now present only while architecture naming is inline,
and dispatches a narrowly rechecked `CapturedEscape` event. The Cancel handler
also restores `Cancelled · Select / move` status; ordinary architecture Escape
delegates to that handler. The only app.rs edits are this subscription entry and
the pre-existing architecture Escape guard delegation.
Two additional regressions exercise actual Iced TextInput update/capture followed
by the subscription mapper and App update: first-keypress Create/Rename cancel
preserves exact accepted model/source/revision/camera, while absent architecture,
armed placement and the existing-element picker do not react to captured Escape.
Ignored Escape and captured Enter are not mapped by the new subscription.
The final `architecture_` gate passed **22 tests**, 569 filtered out, in 4.12 s
after 2m 19s compilation. Log: `.local/palette-escape-tests-20260910.log`.
The same locked desktop build command passed in 1m 12s; log:
`.local/palette-candidate2-build-20260910.log`. Workspace formatting check passed;
log: `.local/palette-candidate2-fmt-20260910.log`.
Second candidate at the same local build path: 109,518,848 bytes, modified
`2026-09-10T17:59:06.6655664Z`.
SHA-256: `E6D6B1D975A11F242D288039E8F07D20E5474EA9C7CFCDE93F1318046EA548D4`.
Root received this identity for a new staging copy. Strict desktop Clippy
(`--locked -p oa-desktop --all-targets -- -D warnings`) passed in 1m 07s;
log: `.local/palette-candidate2-clippy-20260910.log`. Cargo lease is released to
root again. No worker-owned Cargo process remains.
Root's existing native stage remains the earlier `1B47C096…` binary and is not
overwritten. The full integrated suite is deferred until the other-host behavior
and forms changes are integrated; the earlier 588-test scope is preserved above.
@@ -0,0 +1,30 @@
# Independent palette review — 2026-09-10
Reviewer: **Codex-9e8r6t /root/behavior_audit**. Root requested this bounded read-only review while the separate-host scenario patch is being coordinated. Reviewed the current uncommitted architecture delta over `1797516`, its new/existing tests, guide wording, [implementation evidence](release-palette-implementation-20260910.md), and [original native acceptance](release-capella-basic-editing-20260907.md). Read Gitea issue #1, including Codex-956atf's stale-submission finding in comment #12. No production files, Cargo commands or native input were used in this review.
## Confirmed finding
**P2 — stale submission hides the retained inline name.**
`crates/oa-desktop/src/app/architecture_authoring.rs:400–416` validates nonempty input, then sets `saved.placing = true` before calling `commit_architecture`. The commit guard at `447–460` rejects stale document/revision/diagram, dirty source/properties/behavior or invalid point, but it only sets the status string and returns. It does not restore `placing = false` or set the draft error. `State::inline` at `95–99` requires `!placing`, and `inline_editor` at `540–541` filters on that predicate.
Consequently the model remains unchanged and the bytes of the name remain in memory, but the name editor disappears on this rejection. The status says the name is retained while asking the user to cancel/restart. The old `Edit` action was removed in this delta; no direct repair action restores the editor. A later canvas click can reopen naming, but cannot repair the stale document/revision binding. This is a defect in the stale-context recovery path, **not an observed ordinary native workflow failure or accepted-model corruption**. Most normal navigation/model edits are already blocked during naming by `app.rs:1075–1097`.
**Minimal correction:** on the guarded rejection, retain all raw fields and set `saved.placing = false` plus `error = Some(status.clone())`, as the actual execution-failure branches already do at `474–477` and `534–536`. Keep rejection of the stale commit; do not silently refresh its document/revision or accept it against another model. Ensure the retained editor can be read/copied/cancelled. This mirrors the separate-host finding but was independently confirmed in our current implementation.
**Focused falsifier to add:** for both named Create-at-point and F2 Rename, capture the raw name, accepted bundle and camera; invalidate the saved document/revision binding in the test fixture; send `Event::Submit`. Assert the bundle and camera are exact, no new history is created, the same raw name/point remains, `inline()` is true, and `error` is populated. Repeat Submit without repair and assert it still rejects. Cancel must remove only the draft. Restore ordinary naming afterward to ensure the failed draft does not poison the next operation. The existing stale test changes a **Click event**'s revision (`architecture_authoring_tests.rs:65–86`), so it does not cover this Submit path. The four new tests cover blank Submit and invalid Click, not stale Submit.
## Areas reviewed without another confirmed source defect
- The new F2 test drives a real Iced canvas event, requires canvas focus and verifies exact source/history plus unchanged placements/camera. The production Arc borrow fix is correct and does not alter selection identity.
- Named OA/SA allocated work is root-owned with a distinct allocation and placement in one Batch. New tests compare exact source bundles before/after Undo/Redo; no semantic-parent/allocation conflation was found.
- Nonfinite Click rejection precedes saved geometry, and State restoration also validates point finiteness, compatible type/performer and accepted fingerprint. Existing checkpoint tests retain raw names, phase and point with a fresh process binding. These safeguards should stay intact.
- Name length is bounded, whitespace-only Submit keeps the editor/error, and Escape cancels without accepting a placeholder. Choosing another creation tool is allowed only while the first tool is unnamed; named inputs are protected by the App gate.
- The new card height bound and scrollable wrapper directly address a long title/error overflowing the canvas. Source review does not prove native edge placement, focus, scrolling or expanded-label readability. There is no new measured layout assertion for this wrapper; retain root's minimum/wide native acceptance.
- Current guide text teaches side-palette creation, point then inline name/Enter, Escape, F2, explicit allocated work and palette connections. Those statements match the inspected architecture implementation. Scenario guide labels will need synchronization after the separate clicked-slot/layout work; that is outside this palette review.
## Validation boundary
The upstream palette report records desktop588, GUI build, formatting and strict desktop Clippy. This reviewer did not rerun them. Root reports actual tool→point→inline→Enter with stable screen origin and one exact source Undo; that evidence belongs in root's native report. It does not exercise stale submission, long names at the lower/right edge, existing insertion/Locate, recovered inline focus or all OA/SA allocation cases.
Fix and run the focused stale-submission falsifier before claiming that rejection paths retain a usable inline editor. No other blocking finding was established in this bounded review. Scenario production remains unchanged pending root's source-coordination clearance.
+118
View File
@@ -0,0 +1,118 @@
# Palette native acceptance — 2026-09-10
Codex-9e8r6t `/root`, actual Windows Computer Use through the bundled `@oai/sky`
API. This is ongoing acceptance, not a release verdict.
## Candidate and isolation
Initial binary SHA256
`1B47C096624BB1101BEC8B18E63CD601A259DFF8C1BEC24581C31C6B511F9411`,
109,516,288 bytes, staged with the exact LLVM-MinGW libunwind DLL and examples
in `.local/native-qa/palette-20260910-v1/bin`. Process4852 started at
17:42:58UTC with fresh task-local APPDATA/LOCALAPPDATA and a clean Windows PATH.
`process.json` records hashes, paths and process identity; stdout/stderr are
retained there. No pre-existing Arcadia GUI was touched. Native window525310
was selected from actual returned windows.
The frozen independent fixture was copied to the run's `work` directory.
Checker self-check and baseline passed before UI work using the local toolchain
runtime on PATH. The native Open dialog loaded this copy. UIA value setting in
the Windows file dialog was unavailable (`CacheRequest`,0x80070057); actual
focused filename typing succeeded. This was a test-tool issue, not an app error.
## Observed and verified
- At1442×932 outer dimensions, expanding the palette reveals Entity/Actor,
Activity, compatible connections and Add existing. Incompatible drawing tools
explain their missing source. Expansion changes canvas width only.
- Entity tool → point(381,435) → focused inline field → `Rescue team` → Enter
worked. Canvas origin remained at approximately(261,315) throughout all phases;
the accepted card appeared at the chosen screen point. No placeholder model
element was accepted before Enter. Opening a project had retained a camera
pan, so this screen point was not world(120,120).
- Ctrl+S succeeded first submission. The initial fixed-coordinate entity oracle
intentionally failed because the operator had not yet set its required world
position: saved(90,90.20001220703125), expected(120,120). This is not evidence
of a click-transform defect: the observed screen placement matched the click.
The complete click-state bundle is preserved in `checkpoint-entity-click`.
- One Ctrl+Z removed the whole creation. Ctrl+S then passed the complete baseline
model and both exact source files. Preserved in `checkpoint-entity-undo`.
Ctrl+Y restored the same Entity/placement identity visually.
- Through the actual node context menu's Placement properties, the operator
explicitly set X=120,Y=120 and applied. Ctrl+S passed the independent complete
entity model and all **three** source files. Preserved in `checkpoint-entity`.
The existing oracle was not changed to fit the observed result.
- Native border resizing reached1102×732 outer dimensions, corresponding to
the configured1100×700 client minimum. Both expanded156px and compact36px
palettes remained usable. The canvas origin stayed stable; the existing
entity retained its world coordinates and visible camera position.
- Compact Actor tool → lower-right canvas point(730,673) opened the inline
editor clamped wholly within the available canvas. A long Unicode name was
horizontally editable without card overflow; Create/Cancel remained visible.
## Confirmed defect and next verification
**Escape from the focused inline text input does not cancel on the first key.**
It unfocuses the input while retaining the inline card and pending title state.
A separate screenshot observation confirmed the result. `app.rs`'s keyboard
subscription discards captured Escape, and Iced's text input captures it.
Palette worker is implementing narrowly scoped inline cancellation, preserving
other widgets' captured-Escape behavior. This was missed by tests that directly
sent the Cancel event. Stale-Submit visibility was independently found in source
and is being fixed in the same next candidate.
Explicit Cancel subsequently closed the draft; Ctrl+S then passed the complete
entity model and all three exact sources (`checkpoint-cancelled`). The captured
pending recovery snapshot is separately preserved in
`checkpoint-long-draft-recovery`: exact Actor name, point(439,328.2), placing=false.
It is a retained checkpoint for a recovery test, not a claimed crash test.
The Cancel footer still gave the old positioning instruction; that feedback is
also corrected in the next candidate. `escape-retains-inline.png` records the
actual first-Escape failure.
## Scenario layout baseline
Opened the staged teaching example from Home at the same1102×732 outer size.
Its process view uses most of the center pane for the graph. Selected Scenario
`Repair request and measured reply`: its heading/tools pushed the idle diagram
top to approximately483px, leaving only its upper part visible. Choosing
`+ Message` shrank its diagram to approximately110px: only the Controller and
Technician headings fit; the exchanges were below the viewport. The form also
had a small independently scrolling field area. Saved evidence:
`scenario-minimum-before.png` and `scenario-minimum-pending-before.png`.
This confirms the source audit's scenario workspace problem. Explicit Cancel
restored the clean teaching model; no semantic edit was committed.
The first candidate closed cleanly through its native title-bar button. The
subsequent process check found process4852 absent and its stderr log empty.
Next: verify corrected first Escape and restored checkpoint focus, then F2,
named/existing placement, allocation, connection, transitions and history/reopen.
Minimum-size observations alone do not complete those remaining workflows.
## Candidate 2: focused Escape and recovery
The isolated `palette-20260910-v2` stage contains the rebuilt candidate with
SHA-256 `E6D6B1D975A11F242D288039E8F07D20E5474EA9C7CFCDE93F1318046EA548D4`.
Its `process.json` records process 648, the executable and runtime DLL hashes,
and its separate profile/work directories. The candidate 1 stage was preserved.
Home's Recover action restored the retained long Actor-name checkpoint. Both
the complete accepted bundle and serialized architecture draft matched the
preserved checkpoint; the comparison is retained in `checkpoint-restored-recovery`.
The inline card was visible with its full name and position, but recovery did
not automatically focus its name field (`recovered-inline-before-focus.png`).
After clicking that field, **one Escape cancelled the draft**, returned the
palette to Select, and showed `Cancelled · Select / move`. This verifies the
actual captured-key fix, not merely a directly injected Cancel event.
Ctrl+S opened Save As for the recovered copy. Submitting the full path to
`recovered-work/project.arcadia` saved successfully on the first submission.
The independent checker passed the complete Entity model and all three exact
source files; `checkpoint-recovered-save` preserves the result. A read-only
repeat after the desktop-control stop also passed the same oracle.
The user stopped Computer Use with physical Escape during the next observation.
No rename action was performed by that stopped call. Native control is paused;
F2 rename, the remaining complete basic-editing sequence, transitions and final
reopen/runtime acceptance remain pending. Source integration and packaging
continue. Recovery autofocus remains a small confirmed usability follow-up.