Theme-toggle button now paints a Sun glyph in dark mode (click → light) and a Moon glyph in light mode (click → dark); the Sun icon was hardcoded before. Adds Icon::Moon (lucide crescent) and threads theme_mode into TopBar.
Bumps the casement submodule with a fix for the native macOS traffic-light reposition: idempotent absolute placement against resize, baseline invalidation on fullscreen exit, and a poison guard so a transitional re-capture can't drop the lights below their default position.
Add ValidationProviders bundle (approach c) carrying pre_validator /
screenshot / vision trait refs + system_prompt, passed as a new
parameter to Orchestrator::run() and threaded into all 3 execution
paths (sequential, dashboard, concurrent).
Hook site pattern (after run_cleanup_passes + CleanupDone, before
returning RunSummary):
if request.validation_enabled && !abort.is_set() {
run_post_generation_validation(sink, providers.*, &request, …);
}
Update every existing test caller to pass stub ValidationProviders
(SkippedPreValidator / SkippedScreenshotProvider / SkippedVisionLlmClient).
Port of orchestrator.ts:1247-1292.
New tests (run_tests_d1.rs): 8 tests covering all 3 paths × enabled /
disabled / abort-before-hook scenarios.
Port `runPostGenerationValidation` (TS L280-524) into `validation.rs`:
pre-check + node-count gate + 3-round vision loop with fix-history dedup,
abort short-circuit, and all 5 Progress emission points. Adds
`ValidationSummary { total_applied, rounds_run }`. 11 TDD tests in
`validation_tests_c2.rs` cover every gate + edge case (TC-1 through TC-11).
Port buildNodeFromSpec (TS:307-372) + the addChild branch of applyValidationFixes
into Rust. When a StructuralFix::AddChild arrives, build a PenNode from the
vision-LLM spec (frame/text/rectangle/ellipse/path), insert it via
EditorCommand::InsertSubtree under the parent, then call
auto_fix_parent_layout_after_add_child. Status-bar parents are protected.
Also fixes the B2 nit: extract_name_base now lowercases the name before
extracting the last word, matching the TS toLowerCase() call so "Nav ITEM"
and "Tab item" share the same base "item".
Icon resolution is a stub (d/iconId left None); a TODO marks the gap for
a future IconResolver trait. 11 new tests added; full suite 480 green.
Port SAFE_FIX_PROPERTIES (17 entries), is_valid_fix_value, and
is_valid_structural_fix from design-validation-fixes.ts:67-157.
Adds 65 tests covering all property kinds + structural fix shapes.
Port of orchestrator-sub-agent.ts:739-748. When a subtask carries
existing_section_labels (Some(non-empty)), appends the verbatim 9-line
APPEND MODE: block to the sub-agent user prompt, with each label
double-quoted and joined by ", " matching the TS format.
S3b-3 Task C3: branch `run()` to `run_dashboard_path` when
`should_use_dashboard_columns` is true for sequential requests.
Extracts the dashboard implementation into `run_dashboard.rs` to
keep `run.rs` under the 800-line ceiling (606 lines post-split).
- Logical-to-live id resolution: walks live document tree after
`InsertSubtree` via `collect_name_id_map`; resolves Sidebar /
Main Content / "{label} Slot" by name.
- Sets `subtask.generated_root_id` to the slot live id (ports
TS `orchestrator.ts:541` `generatedRootId` fallback).
- Sidebar subtasks → `live_sidebar_id`; others → slot live id.
- 3-attempt tier-gated retry ladder identical to sequential path.
- Post-loop: `reorder_dashboard_main_children` + `run_cleanup_passes`
on both `live_sidebar_id` and `live_main_id`.
- Removes `#![allow(dead_code)]` / `#[allow(dead_code)]` from
`dashboard_columns.rs` and `scaffold_dashboard.rs` (all symbols
now live-reachable).
- 4 new tests in `run_tests_c3.rs`: happy path verifies Sidebar /
Main Content children in live document; non-dashboard regression;
zero-node NoContent; abort-mid Aborted.
329 tests pass; clippy -D warnings clean; fmt check clean.
Add `build_scaffold_dashboard` in new sibling `scaffold_dashboard.rs`
(S3b-3 Task C2). Builds the horizontal root + 260px sidebar +
fill_container main column scaffold; calls
`assign_dashboard_main_parents` to synthesize row/slot frames and
**embeds them as nested children of main** in the same `InsertSubtree`
(necessary because `cmd_insert_subtree` remaps every node id —
references between row / slot / main would break across separate
commands). Returns
`(cmds, sidebar_id, main_id, scaffold_baseline)` where the baseline
matches `descendant_count(state, root_id)` after apply (mirrors TS
`scaffoldCounts.set(rootId, countDescendants(root))` in
`orchestrator.ts:1038-1053`).
Port of `orchestrator.ts:283-320` (createDashboardColumnFrames) +
`orchestrator.ts:954-1019` (useDashboardColumns scaffold branch).
`cleanup::count_descendants` upgraded to `pub(crate)` so the scaffold
can pre-compute the same baseline value. Existing sequential +
concurrent scaffold functions unchanged.
12 new tests in sibling `scaffold_tests_c2.rs`; 325 total green.
Faithful port of TS `normalizeOrchestratorPlan` dashboard branch
(orchestrator.ts:259-272): when `is_dashboard_like_prompt`, overwrite
each subtask's region.width via `infer_dashboard_section_width` and
clamp region.height to [inferred*0.6, inferred*1.6] (replace when ≤0).
Non-dashboard path is unchanged. Six new tests cover width rewrite,
height-kept-in-range, height-replaced-when-zero, clamp-to-max,
clamp-to-min, and non-dashboard-unaffected.
Ports TS orchestrator.ts:486-547 to Rust (Task B3 of S3b-3).
- Add `generated_root_id: Option<String>` field (#[serde(skip)]) to
`Subtask` so run.rs can record the top-level node id post-generation.
- Implement `get_dashboard_placeholder_height` — estimates dashboard
main-column height from first 2-3 rows' tallest subtasks + sidebar,
clamped to [560, 680].
- Implement `reorder_dashboard_main_children` — emits sequential
`EditorCommand::MoveNode` commands to re-sort the main column's
children back to plan order after sub-agent generation.
- Both functions live in the new split module
`dashboard_columns/height_reorder.rs` (207 lines).
- 9 new tests in `dashboard_columns_tests_b3.rs`; suite grows from
298 → 307 passing.
Port TS assignDashboardMainParents (orchestrator.ts:401-484) to Rust.
Implements §4.6: synthesizes fill_container Slot frames for single-subtask
rows, and horizontal Dashboard Row frames + proportional-width Slot frames
for multi-subtask rows (min 220 px per slot, last slot always fill_container,
available_width floor 320 px). Splits §4.6 into dashboard_columns/slots.rs
to keep all files under the 800-line ceiling; TDD test suite in
dashboard_columns_tests_b2.rs (10 tests, 298 total passing).
Implements Task B1 of S3b-3: `normalize_dashboard_main_subtasks` strips
metrics-row phrases from the main-content-container's elements, relabels
it to "Top Bar", and clamps its height to [88, 120].
`group_dashboard_main_rows` is a greedy bin-packer over non-sidebar
subtasks returning `Vec<Vec<usize>>` row groups + full_width + row_gap.
Standalone rule: width >= full_width*0.82 OR is_main_content_container.
Flush thresholds: pre-flush > 1.05, post-flush >= 0.92. row_gap =
root.gap when > 0, else 24. Exact port of orchestrator.ts:322-399.
17 new tests; full suite 288 green; clippy + fmt clean.
Implements Task A2 of S3b-3: ports `inferDashboardSectionHeight`,
`inferDashboardSectionWidth`, and `extractSidebarSurfaceColor` from
`orchestrator.ts:213-237` and `orchestrator-sidebar-color.ts` into
`dashboard_columns.rs`.
- `infer_dashboard_section_height`: sidebar→760, header→96,
metric/kpi→160, chart/revenue→320, transaction/activity/feed→320,
table/analytics/customer→340, default→160.
- `infer_dashboard_section_width`: chart/revenue→main*0.62 (rounded),
transaction/activity/feed→main*0.38 (rounded), else full main_width;
main_width = max(320, root_width-260); sidebar→260.
- `extract_sidebar_surface_color`: catalog "Sidebar Surface | #hex" table
match → inline match → design.md palette sidebar→panel→surface|card
role lookup → None (caller falls back to root fill or #0F172A).
Tests split into `dashboard_columns_tests.rs` (454 lines) via `#[path]`
to keep both files under the 800-line ceiling. 25 new tests, 270 total.
Part 1 (from C1 review): add `OrchestratorError::AllFailed(String)` variant
to replace the misused `Internal` in `aggregate_concurrent_verdict`; update
the doc-comment on that function; update `cleanup_tests_c1.rs` assertions.
Part 2 (Task C2): `Orchestrator::run()` now computes `screen_groups` +
`effective_concurrency` after planning and branches:
- `effective > 1` → N-root scaffold (`build_scaffold_concurrent_mobile`) +
`run_concurrent` + `aggregate_concurrent_verdict` + cleanup; on all-fail
calls `cleanup_concurrent_roots` and returns `AllFailed`.
- `<= 1` → the existing sequential path, completely unchanged.
Inline tests extracted to sibling files (`run_tests.rs`, `run_tests_c2.rs`)
to keep `run.rs` under the 800-line cap. 226 tests pass.
Add `aggregate_concurrent_verdict` (port of orchestrator-sub-agent.ts:319-325):
total_nodes = Σ node_count; Err(Internal(first_error)) iff all zero + non-empty
collected; partial success accepted. Add `cleanup_concurrent_roots` (port of
orchestrator.ts:1101-1158): per-root delete-if-scaffold-only; variable rollback
only when nothing survived across all N roots. 10 new tests in cleanup_tests_c1.rs.
Task B2 of S3b-2: implements `run_concurrent` — the concurrent executor
that drives screen-group workers via a tokio Semaphore (RAII permits,
FIFO-fair, cap = effective_concurrency), collects per-worker buffered
EditorCommands, replays them into the real DocSink in subtask-plan-index
order, and fan-ins Progress events via mpsc channel.
Also splits concurrent.rs test block into concurrent_tests.rs (STEP 0)
to keep both files under the 800-line ceiling, wired via
`#[path = "concurrent_tests.rs"] mod tests` (same pattern as plan_repair).
Updated run_screen_group_worker signature: now takes Arc<Semaphore> +
mpsc::UnboundedSender<Progress> instead of &mut dyn FnMut(Progress).
Added CountingLlm to test_support for semaphore-cap verification.
Added tokio (sync + rt + macros) to op-orchestrator Cargo.toml.
208 tests pass; clippy -D warnings clean; fmt clean.
concurrent.rs: 432 lines; concurrent_tests.rs: 727 lines.
Task B1 of S3b-2. Adds BufferDocSink and run_screen_group_worker to
concurrent.rs, enabling per-worker buffered execution with the 2-attempt
concurrent retry ladder (reduced=false/false → true/true), ready for B2
to wire join_all + serialized replay.
Adds `build_scaffold_concurrent` (+ mobile variant) to `scaffold.rs`.
One root frame per `ScreenGroup`, laid out left-to-right with gap 100;
per-group height = mobile ? root_frame.height (||812) : max(320, Σ region heights);
optional status-bar per group; returns `(cmds, root_ids, baselines)`.
Existing single-root `build_scaffold` is unchanged.
10 new unit tests, 193 total passing; clippy + fmt clean.
Adds DesignRequest.concurrency, group_subtasks_by_screen + ScreenGroup,
effective_concurrency, and clamp_concurrency in a new concurrent.rs module.
Faithful port of orchestrator.ts:780-810 (minus S3b-4 append gate).
All 17 DesignRequest literals updated; 183 tests green.
Part 1: replace hardcoded PLANNING_TIMEOUT / SUBAGENT_TIMEOUT constants
in prompt.rs with profile-derived timeouts from timeouts.rs:
- build_orchestrator_prompt Rich/Minimal → orchestrator_timeouts(prompt_len)
scaled by timeout_multiplier (port of TS fastTimeout=false branch).
- build_orchestrator_prompt Compact → builtin_planning_timeouts(tier)
(port of TS fastTimeout=true / builtin-provider path).
- build_subagent_prompt → sub_agent_timeouts(prompt_len, tier) scaled by
timeout_multiplier (port of getSubAgentTimeouts).
All three CallRequest fields (timeout, no_text_timeout, first_text_timeout)
are now populated; no_text/first_text are Some instead of None.
Part 2: remove stale #![allow(dead_code)] from retry.rs, plan_repair.rs,
and timeouts.rs — their callers have been wired in by C2/C3/this task.
Replace the single-attempt subtask loop with the 3-attempt retry
ladder from orchestrator-sub-agent.ts:128-206 (sequential path).
Attempt 1: reduced_complexity=false, minimal_skills=false
Attempt 2: reduced_complexity=(tier==Basic), minimal_skills=false
Attempt 3: reduced_complexity=true, minimal_skills=true
Retryable = error.is_some() && node_count==0 && !abort.is_set()
&& !is_non_retryable(&err). The non-retryable predicate
is evaluated once from attempt-1's error (faithful to TS).
Partial results (node_count>0) are never retried.
After 3 still-zero the existing zero_node_failure stop applies.
Abort classification preserved.
4 new tests; existing run_zero_node_subtask_stops_and_errors updated
to supply 3 garbage responses (the ladder now consumes up to 3).
166 tests green; clippy + fmt clean; run.rs at 764 lines.
Port of the TS `executeSubAgent` skill-filtering branches and the
`retryAllowed` set from `orchestrator-sub-agent-compact.ts`.
- New `compact_skills.rs`: `apply_skill_filter` + `SkillNamed` trait.
- `minimal_skills=true` → keep only `schema`+`jsonl-format`/`-simplified`.
- `reduced_complexity=true` + `ModelTier::Basic` → keep the 8-skill
`retryAllowed` set (drops `elements`, `overflow`, `icon-catalog`, etc.).
- Standard/Full tier: `reduced_complexity` is a no-op (full set through).
- `build_subagent_prompt` gains `reduced_complexity: bool` + `minimal_skills: bool`
params; resolves tier from the request model and applies the filter after
`resolve_generation_skills`.
- `run_subtask` gains the same two params and threads them into `build_subagent_prompt`.
- All existing callers in `run.rs` and tests pass `false, false` (single-attempt path).
Port parseOrchestratorResponse from orchestrator-planning.ts:20-55.
Three text-extraction strategies (direct / fenced / brace-slice), each
tried strict-then-repair for six probes total; repaired=true when the
result came from the repair path. Fence extraction uses plain string ops
(str::find / strip_prefix / strip_suffix) — no regex crate added.
Task B2: appends repair_plan_object, finalize_plan,
extract_subtask_candidates, coerce_subtask, build_fallback_heights
to plan_repair.rs. Extends Subtask with optional elements/screen
fields and updates four existing struct literals accordingly.
Adds 12 new tests; full suite 140 tests green.
OrchestratorPlan now accepts the TS-native rootFrame/fill-array/styleGuideName
shape emitted by decomposition.md. Removes StyleGuide struct; adds PlanFill +
first_solid_hex helper. variables.rs enters dormant state (no palette to seed).