A view-scoped ModelCommand names its view (improvise-lltt)

Nine ModelCommand variants configure a view and named it implicitly: the
effect read App::view_mut(), the HOST's slice. Since improvise-jytm made
ViewState::active_view authoritative per client, a client on another view
moved the host's view instead of its own -- its grid did not change and
every other client's did.

Each of the nine now carries `view: String`; model_effects maps it to
effect::ViewTarget::Named, and the nine effects carry a ViewTarget rather
than reading an ambient one. There is no operandless form left:
App::view_mut() is deleted and replaced by view_mut_of(&ViewTarget), so a
future geometry effect has to say which view it writes. The host's key
path passes ViewTarget::Active, which is right -- the TUI is the host and
has no session to ask. A Named view the document has not got is a no-op
rather than resolve_view's fallback onto the default.

Second half, which the ticket did not predict: naming the view made a
reframe of a view the host is NOT on reachable, and adopt_cursor_epoch
could not see one -- cursor_epoch is bumped only by the host's own
reset_cursor, so those clients were reset by nobody and told nothing
(gotcha 24's strictly-worse-than-nothing state). App::last_reframed_elsewhere
is the second trigger; its opening frames are taken lazily inside
view_mut_of, the one door such a write goes through, so the held-key path
still hashes one view and improvise-mcq4's declined cost is not paid.

Evidence and the three injected faults: context/test-evidence/command.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-21 10:23:27 -07:00
parent ce895ff560
commit 1dd6950ed3
19 changed files with 671 additions and 129 deletions
+5 -2
View File
@@ -8,7 +8,7 @@ use improvise::model::cell::{CellKey, CellValue};
use improvise::view::Axis;
use improvise::workbook::Workbook;
use improvise_command::ui::app::App;
use improvise_command::ui::effect::{CycleAxis, Effect, SetCell};
use improvise_command::ui::effect::{CycleAxis, Effect, SetCell, ViewTarget};
/// The fixture `grid_projection` uses, so the two numbers are comparable.
fn dense_app(regions: usize, products: usize, measures: usize) -> App {
@@ -108,7 +108,10 @@ fn geometry_at(
let (rows, cols) = (app.layout.row_count(), app.layout.col_count());
let mut best = Duration::MAX;
for i in 0..iters.max(3) {
let effects: Vec<Box<dyn Effect>> = vec![Box::new(CycleAxis("Product".to_string()))];
let effects: Vec<Box<dyn Effect>> = vec![Box::new(CycleAxis(
ViewTarget::Active,
"Product".to_string(),
))];
let start = Instant::now();
std::hint::black_box(app.apply_effects(effects));
best = best.min(start.elapsed());