Commit graph

513 commits

Author SHA1 Message Date
Fini 8a39bc7b03 Merge branch 'v0.8.0-new' of github.com:ZSeven-W/openpencil into v0.8.0-new 2026-05-24 00:39:09 +08:00
Kayshen-X cb9a0ae692 fix(ci): clear rust check failures 2026-05-24 00:29:43 +08:00
Kayshen-X 44c2b990fa feat(editor): enable figma import drop zone 2026-05-24 00:29:05 +08:00
Kayshen-X caa53e55ff chore(test): format golden fixtures 2026-05-23 23:55:00 +08:00
Kayshen-X 66b6330914 fix(canvas): render image fills on canvas 2026-05-23 23:48:11 +08:00
Kayshen-X 0948e97de9 feat(panels,canvas): editable gradients/effects + SVG/image import + locale-aware dialogs
Continues the gradient + property-panel polish from the previous
commit and rounds out two new flows the TS app already has:

Gradient stops + effects:
- ColorTarget gains GradientStop(i) + EffectColor(i); HSV picker
  preserves alpha across hue/SV drags so a transparent stop stays
  transparent. Hex pill stays 6-char; alpha is reattached at commit
  and the swatch sits on a 2x2 alpha checker so #00000000 reads as
  transparent rather than empty.
- Effects section reflowed into card-style blocks (image #9 spec):
  title + minus, X/Y and Blur/Spread 2-col grids, color row with
  swatch + rgba(...) text; clicking the swatch opens an HSV picker
  bound to that effect index via SetEffectColor.
- Press dispatch on both hosts anchors picker overlays at the
  clicked y so they pop adjacent to the swatch instead of the top.

Image + SVG import (toolbar + Fill section "图片" row):
- New FileAction::ImportImageOrSvg / PickFillImage; persistence_image
  pops rfd, decodes raster as data: URL, inserts an Image node or
  rewrites the selected node's primary fill.
- ImageNode actually renders on the canvas: NodePayload + SceneNode
  carry image_src, canvas_viewport_paint.rs decodes the data URL
  once and hands raw bytes to RenderBackend::draw_image with a
  src-hash cache id. Grey placeholder paints only when decode fails
  so transparent PNGs don't get a grey matte underneath.
- SVG import ported to TS-parity (packages/pen-engine svg-parser):
  recursive <g> tree walk with inherited fill/stroke/style="...",
  viewBox-aware scaling with maxDim cap, multi-subpath split, raw
  d preserved on PathNode. Imports land wrapped in a Group named
  after the source file.

Locale-aware first run:
- settings_io detects the OS locale (LC_ALL/LANG/LC_MESSAGES with
  zh-Hans/zh-Hant heuristics) and seeds editor_ui.locale before
  settings.json is read; persisted user choice still wins.
- macOS bundle declares CFBundleLocalizations + AllowMixedLocalizations
  so NSOpenPanel / NSSavePanel render in the same language as the
  rest of the chrome.

Web host kept exhaustive across the new variants (PickFillImage,
OpenEffectColorPicker, ColorTarget::EffectColor, GradientStop). Two
new files: persistence_image.rs (file-pick handlers, ≤120 lines) and
svg_path_data.rs (path-d tokenizer + bbox + normaliser, split from
svg_import.rs to stay under the 800-line cap). 277 op-editor-core
tests pass.
2026-05-23 23:11:38 +08:00
Fini 5c161e2211 fix(orchestrator): port extractHtmlFromResponse — strip code fence + wrap bare
Codex stop-time review caught a regression: `generate_design_code`
returned the LLM response verbatim, missing the TS
`extractHtmlFromResponse` post-processing (design-code-generator.ts
L54-87). Without it, the common LLM output shape — ```html
<!DOCTYPE html>... ``` — leaks fence markers into
`extract_structure_summary`'s downstream regex scan.

Port the 4-stage fallback chain:
1. ```(html)?\s*\n?…\n?``` fence — if inner contains
   <!DOCTYPE or <html, return inner trimmed.
2. Trimmed response itself starts with <!DOCTYPE / <html → return as-is.
3. Case-insensitive find <!DOCTYPE…</html> → return slice (original case).
4. Wrap bare content in default <!DOCTYPE html><html lang=en>… scaffold.

8 new tests cover each stage + edge cases (empty, bare text,
case-insensitive doctype, fence without HTML marker, leading whitespace).
Existing `generate_design_code` tests still pass — the scripted
`<html><body>...</body></html>` outputs go through stage 2 verbatim,
identical to the old behaviour.
2026-05-23 21:02:54 +08:00
Fini 23c0f2e4d1 fix(workspace): unblock CI — host visual_ref_enabled + radial-gradient clippy allow + fmt catch-up
Three independent CI-unblockers grouped to keep history clean:

1. op-host-desktop/chat_orchestrator.rs — add `visual_ref_enabled: false`
   to the DesignRequest literal so the workspace builds after S4 added
   the field. Same stub-plumbing pattern as the S3b-2 `concurrency`,
   S3b-4 `append_context`, S3c `validation_enabled` additions. Real
   intent-gate routing is task #27.

2. op-editor-core/render_backend.rs::fill_round_rect_radial_gradient —
   #[allow(clippy::too_many_arguments)] on the trait default. The
   radial-gradient hook carries (rect, radius, stops, cx_frac, cy_frac,
   radius_frac, opacity) per the TS pen-renderer contract; that's the
   data shape, not refactorable without splitting the trait.

3. cargo fmt --all catch-up on 6 other files (op-editor-core /
   op-editor-ui / op-host-native) that pre-dated this branch and were
   never run through rustfmt. Pure whitespace, no semantic change. Lets
   `cargo fmt --all -- --check` pass workspace-wide.

`cargo clippy --workspace --all-targets -- -D warnings` + `cargo build
--workspace` + `cargo fmt --all -- --check` all clean. Tests across
op-orchestrator (574+1) / op-design-lint (149) / op-mcp (144) /
op-editor-core (273) / op-host-desktop (96) all green.
2026-05-23 20:43:39 +08:00
Fini 84241b7ea2 style(orchestrator): post-S4 clippy nits — manual_strip + flatten test modules
Three clippy warnings surfaced by the final-review workspace clippy
pass that the per-task gates didn't catch:

- design_system.rs::extract_code_fence — replace manual slice
  `&after_open[4..]` with `strip_prefix("json")`. Same behaviour,
  cleaner intent, satisfies clippy::manual_strip.
- design_system_tests.rs + visual_ref_tests.rs — flatten the inner
  `#[cfg(test)] mod tests { ... }` wrapper. The files are already
  wired via `#[path = "..."] mod tests;` in their parent modules
  (design_system.rs / visual_ref.rs), so the inner wrapper was double
  nesting (clippy::module_inception). Matches the established
  pattern in concurrent_tests.rs / plan_repair_tests.rs / run_tests.rs.

574 tests still pass. clippy + fmt + cargo build --workspace clean.
2026-05-23 20:43:16 +08:00
Fini edfa599006 feat(orchestrator): visual_ref — execute_visual_ref_orchestration 5-stage flow
Port executeVisualRefOrchestration from visual-ref-orchestrator.ts:45-166.
5-stage flow: design-system seed → HTML codegen → screenshot → enhanced
prompt → Orchestrator::run. Emits VisualRefStarted/DesignSystem/HtmlGenerated/
ScreenshotReady/Fallback progress variants. Falls back to plain Orchestrator::run
on empty HTML or None screenshot. Abort checked before each stage.
2026-05-23 20:31:05 +08:00
Fini bb6c980983 feat(orchestrator): visual_ref — generate_design_code + structure summary + enhanced prompt
S4 B2: port three HTML helper functions from the deleted TS visual-ref
pipeline. generate_design_code uses design-code + design-principles skills
as system prompt. extract_structure_summary uses a hand-rolled byte scanner
(no regex crate dep) to extract sections/headings/CTAs. build_enhanced_prompt
produces the byte-exact TS prompt template. 20 new tests; 569 total green.
2026-05-23 20:21:05 +08:00
Fini 5c14dc3597 feat(orchestrator): design_system — generator + variable seeding + prompt context
Task B1 of S4. Adds three functions to design_system.rs:
- generate_design_system(): async LLM call with design-system skill as system
  prompt; parses response via parse_design_system; falls back to DEFAULT on
  any failure (port of design-system-generator.ts:20-29).
- design_system_to_seed_commands(): emits SetVariableColor/SetVariableScalar
  for 8 palette tokens (color-*), 6 spacing steps (spacing-xs..2xl), 3 radius
  steps (radius-sm/md/lg), 2 font strings (font-heading/font-body), and 6 type
  scale steps (font-size-1..6) — 25 commands for DEFAULT (port of TS:134-156).
- design_system_to_prompt_context(): byte-exact port of the TS template prose
  "DESIGN SYSTEM (use these values consistently):\nColors: bg ... Style: ..."
  (port of TS:161-170).

11 new TDD tests; full suite 550 green (baseline was 539).
2026-05-23 20:13:53 +08:00
Fini 8444d4355c feat(orchestrator): types — VisualRefProvider trait + SkippedVisualRefProvider stub 2026-05-23 20:03:55 +08:00
Fini 1a601eca70 feat(orchestrator): design_system — types + parse + defaults + Progress + visual_ref_enabled
Port TS design-system-generator.ts (deleted in 0f12b6e9) into Rust as Task A1 of S4:

- Add design_system.rs: DesignSystem struct (palette BTreeMap + Typography + Spacing + radius Vec<f64> + aesthetic), default_design_system() OnceLock accessor, parse_design_system() 4-stage fallback chain (direct → code-fence strip → brace-extract → DEFAULT).
- Add 5 Progress::VisualRef* variants to types.rs: VisualRefStarted, VisualRefDesignSystem{var_count}, VisualRefHtmlGenerated{byte_len}, VisualRefScreenshotReady{skipped}, VisualRefFallback{reason}.
- Add DesignRequest.visual_ref_enabled: bool with #[serde(default="default_visual_ref_enabled")] = false.
- Update all 35 DesignRequest struct literals across the crate to include the new field.
- 17 new tests covering all 4 parse stages, DEFAULT exact values, serde round-trip, and variant compilation.
2026-05-23 19:57:30 +08:00
Fini 6a221079ba style(workspace): oxfmt catch-up — 8 pre-existing fixture/golden/tool files
oxfmt across the workspace was failing the pre-commit gate on 8 files
that predate this branch — S1 op-design-lint fixtures + S3b-1a planner
golden + planner dump-golden TS tool — none of which had ever been run
through oxfmt. Pure whitespace/trailing-newline normalization, no
semantic changes. Lets format:check pass cleanly across the workspace.
2026-05-23 19:45:53 +08:00
Kayshen-X 8851746e5e feat(canvas): linear + radial gradient rendering with editable stops
Wires the linear/radial gradient fill type from the property panel
through to a real skia shader on the canvas — previously switching
fill type only changed the model and the panel painted hardcoded
placeholder stops. Stop hex (with alpha preserved), angle, offset %,
and per-stop HSV picker are all live.

- SceneNode + NodePayload carry a resolved gradient body; native
  skia backend builds `linear_gradient` / `radial_gradient` shaders
  with TS-renderer geometry (angle - 90° ellipse projection, radius
  fraction of max(w, h)).
- PropertyFocus + EditContext gain GradientAngle / GradientStopHex /
  GradientStopOffset; commit, snapshot, hit-test, paint, keyboard
  validation, and the property-panel layout walker thread the new
  variants end-to-end.
- ColorPickerState carries a per-session alpha so the gradient-stop
  picker keeps transparency across HSV drags; hex pill stays 6-char
  and re-attaches the stop's existing alpha at commit. Swatch paints
  over a 2×2 checker so a fully transparent stop reads as such.
- skia.rs split into a spine + sibling gradient.rs to stay under the
  workspace 800-line cap.

# Conflicts:
#	crates/op-editor-core/src/lib.rs
2026-05-23 19:13:35 +08:00
Fini 8711641c56 feat(orchestrator,host): production-visible Skipped*Provider + unblock chat_orchestrator compile
Move the three S3c validation stub providers (SkippedPreValidator,
SkippedScreenshotProvider, SkippedVisionLlmClient) from cfg-test-only
test_support to a new pub module 'stub_providers'. Test code keeps
working via a pub(crate) re-export; host code can now construct a
no-op ValidationProviders bundle without re-implementing the trait
impls per host.

chat_orchestrator.rs (predecessor of task #27 'wire intent gate into
chat runtime') compiles again after S3b-2/S3b-4/S3c added required
fields + a required Orchestrator::run arg:

- DesignRequest literal grew append_context: None, concurrency: 1,
  validation_enabled: false (conservative — validation gated off
  until host wires real screenshot / vision LLM).
- Orchestrator::run takes a 6th &ValidationProviders<'_> arg; we pass
  a bundle of the three Skipped*Provider stubs.

The whole module is marked #![allow(dead_code)] because no caller has
yet been added to chat_runtime.rs — that's #27 proper. Delete that
allow in the same PR as #27 真接线.
2026-05-23 18:53:22 +08:00
Fini b58cb463f6 fix(ai): keep debug_validation_report out of the production MCP catalog
debug_validation_report was registered in rebuild_registry and listed
in tools/list unconditionally — only call() consulted
OPENPENCIL_DEBUG_TOOLS. A production client (flag unset) still saw the
debug tool in its catalog and could invoke it, getting a bare
ToolFailed. That leaks the debug surface into the production catalog.

Gate it everywhere instead of only at call time: rebuild_registry
registers the tool only when debug_tools_enabled(), and
tools_list_response appends the debug schema — now a separate
DEBUG_TOOL_SCHEMAS const, removed from TOOL_SCHEMAS — only when the
flag is set. A client without the flag never sees the tool at all,
matching the TS design where debug tools ship only in a debug build.

debug_tools_enabled() is promoted to pub and re-exported from op-mcp.
The tools/list catalog test now exercises both gate states (82 tools
flag-off, debug tool present flag-on).
2026-05-23 18:39:09 +08:00
Fini 8cff5abaf5 feat(ai): implement op-design-lint Rust crate (S1)
Port the pen-ai-skills diagnostics layer to a new pure Rust crate
`op-design-lint`: 14 design-lint detectors, the detect_all aggregator,
apply_fixes / detect_and_fix, and golden parity tests against the TS
oracle. Wire it into op-mcp as the read-only debug_validation_report
tool, gated by OPENPENCIL_DEBUG_TOOLS=1.

Detectors: empty_paths, unexpected_rotation, excessive_frame_effects,
invisible_containers, text_explicit_heights, text_effect,
text_corner_radius, text_stroke, text_bg_contrast, edge_section_padding,
stacked_horizontal_padding, sibling_inconsistencies (+ check_consistency),
detect_all.

Also includes: node_util shared helpers + pen-core color/visibility
ports, node_mut field accessors, set_property issue->node mutation
dispatch, golden fixture corpus + TS dump script, structural-parity
test, a CI golden-drift guard, and the gitignore fix so the fixture
docs/ dir is tracked.

This branch's per-commit history was squashed: the original 28 commits
carried fabricated timestamps and could not be honestly reconstructed,
so the work is recorded as a single commit at its real completion time.
2026-05-23 18:39:08 +08:00
Fini 3ff8d92601 Merge branch 'v0.8.0-new' of github.com:ZSeven-W/openpencil into v0.8.0-new 2026-05-23 14:29:26 +08:00
Kayshen-X 46eeaef226 feat(panels): editable inputs (arrows, caret, fill-opacity, effect params) + paint polish
Property-panel input editing now supports:
- Arrow keys: Up/Down step a numeric field; Left/Right move the text caret. Caret position is a real index into the draft, so typing inserts at the caret and Backspace deletes the char before it (not just append/pop).
- Layer opacity: full-width box with the localized 不透明度 label inside on the left, value next to it, % at the right edge; clipped so a long-locale label can't bleed past the half-width box.
- Fill opacity: new SolidFillBody.opacity getter / setter on the model + PropertyFocus::FillOpacity + the 100 % box in the Fill head row is now editable end-to-end.
- Effect-param values: each Drop Shadow X / Y / Blur / Spread cell is a click-to-type input box (new effect_param_focus state, FocusEffectParam action, commit_effect_param_focus_if_any path). The − / + steppers still work alongside. Web's apply_property_action makes the focus a no-op (no keyboard path on web yet) to avoid stranding focus.

Paint polish:
- Standardised input-text baselines to + 19.0 across prefix / suffix / icon helpers, the fill / stroke / opacity / hex paints, and the export section so every INPUT_HEIGHT row reads on the same baseline.
- Icon-prefixed inputs now have 10 px left padding (matching X / Y / W / H) and the icon is vertically centred ((30 - 14) / 2 = 8) instead of sitting at y + 5.
- Fill / stroke swatches vertically centred in their hex rows ((30 - 16) / 2 = 7).
- '-' / '#' caret-aware validation in apply_text so typing them at caret 0 of a non-empty draft is now a valid edit. Native input_tests seed property_caret_pos to mirror real focus state.

Codex review iterations: poison-guard the effect-param focus on web, content-clip the layer-opacity row, fix property-panel scroll clamping in paint, and a few related safety guards across hosts.
2026-05-23 12:59:28 +08:00
Kayshen-X 2efca5aa3b feat(topbar): sun/moon theme toggle icon + traffic-light reposition fixes
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.
2026-05-23 12:59:02 +08:00
Fini 4557a61bd5 feat(orchestrator): run — wire vision validation across all paths
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.
2026-05-23 09:23:19 +08:00
Fini 9a8f8b6013 feat(orchestrator): validation — run_post_generation_validation loop
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).
2026-05-23 09:12:25 +08:00
Fini 2a709d6ed4 feat(orchestrator): validation — parse_response + per-round vision call
C1 of S3c: creates validation.rs with parse_validation_response (JSON
cleaning, tool_use block stripping, fix/structural-fix filtering, quality
score clamping) and validate_design_screenshot_with_image (message
builder, timeout-doubling rule when reference screenshot present, trait
dispatch, skipped propagation). 18 unit tests in validation_tests_c1.rs.
2026-05-23 09:04:11 +08:00
Fini e7ba3696a5 feat(orchestrator): validation_fixes — addChild structural + extract_name_base lowercase
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.
2026-05-23 08:49:57 +08:00
Fini 17f960b8d7 feat(orchestrator): validation_fixes — apply property fixes + parent-layout auto-fix
Port applyValidationFixes (TS lines 163-301) and
autoFixParentLayoutAfterAddChild (TS lines 378-433) to Rust.

- New `EditorCommand::SetNodeLayoutProp` + `LayoutPropValue` enum in
  op-editor-core for generic layout/text property writes (gap, padding,
  alignItems, justifyContent, textAlign, textGrowth, opacity, etc.)
- `apply_validation_fixes`: whitelist + value guard, fit_content→pixel
  guard, virtual-property translation (fillColor/strokeColor/strokeWidth),
  status-bar protection on removeNode, addChild deferred to B3
- `auto_fix_parent_layout_after_add_child`: sibling name-base matching,
  child-count guard, justifyContent + gap alignment
- Split to validation_fixes_apply.rs + validation_fixes_b2_tests.rs to
  stay under 800-line ceiling
- 469 tests pass; clippy + fmt clean
2026-05-23 08:36:34 +08:00
Fini deb99624d4 feat(orchestrator): validation_fixes — whitelist + validators
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.
2026-05-23 08:13:17 +08:00
Fini 4882277b7f feat(orchestrator): validation_dump — node tree dump + count 2026-05-23 08:06:10 +08:00
Fini c098effac6 feat(orchestrator): types — vision validation traits + Progress + config
Add the S3c type layer: 3 injected traits (PreValidator / ScreenshotProvider /
VisionLlmClient), supporting types (PreValidationResult / VisionCallRequest /
VisionResponse), 5 new Progress::Validation* variants, DesignRequest.validation_enabled
field (serde default true), and validation_config.rs constants faithful to
ai-runtime-config.ts:119-123 (node-count threshold 30, max rounds 3, quality
threshold 8, timeout 180 000 ms). Stub impls (Skipped*) added to test_support.rs.
All 35 existing DesignRequest literals updated with the new field. 366 tests pass.
2026-05-23 07:52:59 +08:00
Fini 56f56eab0b feat(orchestrator): run — wire append-to-document mode
4 call sites in Orchestrator::run():
1. apply_append_context_to_plan after planning_loop (TS :737)
2. skip_status_bar guards effective_is_mobile in build_scaffold (TS :743)
3. effective concurrency forced to 1 when skip_root_insertion (TS :806-810)
4. append fast-path: skip build_scaffold, set parent_frame_id = target,
   capture scaffold_baseline before sub-agent loop (TS :942-949)

Cleanup natural-no-op comment added (spec §4.5). Concurrent + dashboard
paths unchanged. 5 new TDD tests in run_tests_b4.rs.
2026-05-23 07:37:18 +08:00
Fini 0554d05825 feat(orchestrator): subagent — APPEND MODE prompt injection
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.
2026-05-23 07:15:33 +08:00
Fini efcb345faa feat(orchestrator): append — apply_append_context_to_plan + status-bar filter 2026-05-23 07:11:25 +08:00
Fini 25b3b8bf39 feat(orchestrator): types — AppendContext + append_context + existing_section_labels 2026-05-23 07:05:23 +08:00
Fini 3484bd3783 feat(orchestrator): run — wire dashboard column layout
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.
2026-05-23 06:34:50 +08:00
Fini ed817af042 feat(orchestrator): dashboard column scaffold
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.
2026-05-23 06:15:09 +08:00
Fini 0049755af0 feat(orchestrator): plan_normalize — dashboard section-size branch
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.
2026-05-23 05:53:12 +08:00
Fini a3c457ec2c feat(orchestrator): dashboard_columns — placeholder height + child reorder
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.
2026-05-23 05:46:01 +08:00
Fini 8ab8db7b35 feat(orchestrator): dashboard_columns — slot assignment
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).
2026-05-23 05:29:10 +08:00
Fini 9417a62692 feat(orchestrator): dashboard_columns — main-subtask relabel + row bin-packing
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.
2026-05-23 05:11:04 +08:00
Fini 64457e1232 feat(orchestrator): dashboard_columns — section sizing + sidebar color
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.
2026-05-23 04:58:45 +08:00
Fini a1a0ffca12 feat(orchestrator): dashboard_columns — detection predicates 2026-05-23 04:48:12 +08:00
Fini 388f956395 style(orchestrator): drop stale dead-code allows after S3b-2 wiring 2026-05-23 04:37:01 +08:00
Fini 27b5538f72 feat(orchestrator): run — branch sequential vs concurrent path
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.
2026-05-23 04:27:50 +08:00
Fini e43e96ad15 feat(orchestrator): concurrent run-all-aggregate failure policy + N-root cleanup
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.
2026-05-23 04:17:07 +08:00
Fini 7f5152ac07 feat(orchestrator): concurrent executor — semaphore + serialized replay
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.
2026-05-23 04:06:51 +08:00
Fini 4def1dd62b feat(orchestrator): concurrent screen-group worker + buffer DocSink
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.
2026-05-23 03:38:52 +08:00
Fini 3523a4c71f feat(orchestrator): N-root-frame concurrent scaffold
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.
2026-05-23 03:31:48 +08:00
Fini 3643d1fa0e feat(orchestrator): concurrency decision + screen grouping
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.
2026-05-23 03:22:04 +08:00
Fini ebd3546f7f feat(orchestrator): wire timeout profiles into CallRequest builders
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.
2026-05-23 03:03:28 +08:00