Commit graph

1148 commits

Author SHA1 Message Date
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
Fini 08d6feb601 feat(orchestrator): run — sub-agent tier-gated retry ladder
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.
2026-05-23 02:52:54 +08:00
Fini 599e9b4ffa feat(orchestrator): run — planning mode-rotation retry loop
Replace the single hardcoded-Rich planning call with the mode-rotation
retry loop (S3b-1b Task C2). Resolves tier via model_profile, loops
attempt_modes(tier) ([Rich]/[Rich,Minimal]/[Rich,Minimal,Compact]),
handles stream-error rotation, abort short-circuit, parse failure
continue/fall-through, compact forced_style_guide_name backfill, and
exhaustion → build_fallback_plan. Adds tracing dep for warn! diagnostics.
2026-05-23 02:48:00 +08:00
Fini b3c462cd38 feat(orchestrator): subagent — reduced_complexity + minimal_skills skill gating
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).
2026-05-23 02:36:23 +08:00
Fini 474f58186b feat(orchestrator): plan_repair — parse_orchestrator_response cascade
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.
2026-05-23 02:23:17 +08:00
Fini 959da4ec09 feat(orchestrator): plan_repair — repair_plan_object + finalize
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.
2026-05-23 02:18:58 +08:00
Fini a161b9ea6b feat(orchestrator): plan_repair — value coercers 2026-05-23 02:04:50 +08:00
Fini e416ee1cf6 feat(orchestrator): timeouts — per-bucket/tier timeout profiles
Port ORCHESTRATOR_TIMEOUT_PROFILES, SUB_AGENT_TIMEOUT_PROFILES, and
builtin-planning timeouts from the TS ai-runtime-config / prompt-optimizer /
model-profiles trio into crates/op-orchestrator/src/timeouts.rs.

- Timeouts { hard, no_text, first_text } + PromptBucket enum
- bucket_for(len): Short(<2200) / Medium(<4200) / Long
- orchestrator_timeouts, sub_agent_timeouts (Basic-tier clamps), builtin_planning_timeouts
- apply_profile_to_timeouts (multiplier scaling, Math.round parity)
- CallRequest gains no_text_timeout/first_text_timeout: Option<Duration>
  (all existing literals updated to None)
2026-05-23 01:59:27 +08:00
Fini 56c99f5aa8 feat(orchestrator): retry — attempt_modes + is_non_retryable 2026-05-23 01:52:57 +08:00
Fini 1fa2d9415a Merge branch 's3a-orchestrator' into v0.8.0-new 2026-05-23 01:27:30 +08:00
Fini 905c6b1890 test(orchestrator): TS-parity harness for compact planner prompt 2026-05-22 23:12:53 +08:00
Fini deec760175 feat(orchestrator): mode-aware build_orchestrator_prompt + PlanningPrompt
- Add PlanningPrompt to types.rs (carries CallRequest + forced_style_guide_name + mode)
- Rewrite build_orchestrator_prompt: Rich/Minimal resolve_skills + style-guide context
  + suffix; Compact delegates to build_compact_planning_prompt
- Add RICH_SUFFIX / MINIMAL_SUFFIX / planning_suffix / filter_planning_skills_for_prompt
- Delete PLAN_FORMAT const (replaced by the suffix approach)
- Update run.rs phase-1 caller to PlanningMode::Rich + .call_request
- Re-export build_orchestrator_prompt from lib.rs
- Remove blanket #![allow(dead_code)] from style_guide_context.rs; replace with
  per-field #[allow(dead_code)] on diagnostic fields pending S3b-1b consumption
2026-05-22 22:56:28 +08:00
Fini 24b76d2065 feat(orchestrator): DesignRequest carries design_md 2026-05-22 22:56:27 +08:00
Fini 96b014c3b1 feat(orchestrator): compact_prompt — build_compact_planning_prompt 2026-05-22 22:56:26 +08:00
Fini fc25d8c506 feat(orchestrator): build_planning_style_guide_context 2026-05-22 22:56:25 +08:00
Fini 3685bb0178 feat(orchestrator): PlanningMode + style-guide metadata/snippet formatters 2026-05-22 22:56:24 +08:00
Fini bbc60db4ad feat(orchestrator): style_guide_context — weighted style-guide ranking 2026-05-22 22:56:23 +08:00
Fini 97519ea3b5 feat(orchestrator): style_guide_context — infer_tags_from_prompt 2026-05-22 22:56:22 +08:00
Fini af84103780 feat(orchestrator): design_md_policy — design.md planner helpers 2026-05-22 22:56:21 +08:00
Fini 592cb9fa16 refactor(orchestrator): migrate plan schema to TS-native shape
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).
2026-05-22 22:56:20 +08:00
Fini a654ed6800 feat(orchestrator): design_type — detect_design_type + presets 2026-05-22 22:56:19 +08:00
Fini 55a88bdfda feat(orchestrator): model_profile — MODEL_PROFILES tier table 2026-05-22 22:56:18 +08:00
Fini 15b3b28cad style(orchestrator): rustfmt op-orchestrator + InsertSubtree signature
Local verification pass: with a temporary design-md stub the S3a
crates compile against the available jian. op-editor-core (262 tests,
incl. 8 InsertSubtree) and op-orchestrator (31 tests) all pass; clippy
clean. This commit folds in the rustfmt diffs that surfaced.
2026-05-22 22:56:17 +08:00
Fini d170bc9cb3 style(ai): rustfmt chat_orchestrator.rs (Codex review) 2026-05-22 22:56:16 +08:00
Fini 53ba042246 feat(ai): chat_orchestrator — DesktopLlmClient + DesktopDocSink host wiring
Host-side impls of op-orchestrator's two trait seams:
- DesktopLlmClient: implements LlmClient over agent::QueryEngine —
  each call() builds a fresh engine (isolated context per spec §3.2),
  spawns it on the shared runtime, and bridges agent Events to
  LlmChunk via a futures mpsc channel.
- DesktopDocSink: implements DocSink over op_editor_core::EditorState.
- run_design_request: the post-intent-gate entry that runs
  Orchestrator::run().

NOT verified — op-host-desktop is casement-blocked (and op-editor-core
jian-blocked). Two integration points are left to the host author and
documented in the module header: (1) the intent gate in
chat_runtime.rs (classify_intent -> design vs chat), kept untouched to
avoid blind-editing the threading model; (2) DocSink undo-batch wiring
to op-editor-core's History batch API.

S3a Plan C Task 6 (partial).
2026-05-22 22:56:15 +08:00
Fini 20239658df feat(orchestrator): implement cleanup passes — status-bar dedup + height fit
run_cleanup_passes now does two of the three TS cleanup passes:
- remove_duplicate_status_bars: keeps the first status-bar child
  under each root, deletes the rest.
- adjust_root_height_to_content: sets root frame height to the sum
  of its direct children's pixel heights.

Signature gains root_ids: &[&str] so S3b's concurrent multi-root
path reuses it (spec §9). unwrapSingleComponentSectionRoot left as a
documented follow-up (heuristic-heavy).
2026-05-22 22:56:14 +08:00
Fini 8c10bc70fc fix(orchestrator): classify mid-stream abort correctly; fix happy-path test
Codex stop-time review found two bugs:
- A subtask aborted mid-stream returns node_count==0, which the run
  loop classified as zero_node_failure (error path) — wrongly removing
  the scaffold root and returning NoContent. Now checks abort.is_set()
  after run_subtask: an abort during the call takes the abort path
  (keeps the root, returns Aborted).
- The happy-path test asserted root_frame_id == "root", but
  InsertSubtree remaps every id so the live root id is fresh. Assert
  it is non-empty instead.
2026-05-22 22:56:13 +08:00
Fini 1bdf4662a6 fix(orchestrator): Codex review pass — id-remap, raw strings, apply guard
Codex reviewed the blind-written S3a code against the real
op-editor-core / jian-ops-schema APIs and fixed:

- run.rs: InsertSubtree remaps every node id, so the planned
  root_frame.id is NOT the live id. Capture the actual root id from
  active_children() after the scaffold insert and rewrite each
  subtask's parent_frame_id to it.
- run.rs: a rejected scaffold InsertSubtree was treated as success;
  now rolls back + ends the undo batch + returns Internal error.
- run.rs / prompt.rs: r#"..."# raw strings broke on the "# inside
  embedded JSON ("fill": "#RRGGBB"); switched to r##"..."##.
- cleanup.rs: test asserted the literal "root" id; reads the
  remapped id instead.
- prompt.rs: sub-agent NODE_FORMAT now states fields are camelCase
  (cuts runtime parse failures from snake_case model output).

Still unverified — vendor/jian unpushed (see prior commits). Codex
confirmed scaffold.rs's PenNode JSON shape (type tag, camelCase,
SizingBehavior bare number, PenFill) is correct.
2026-05-22 22:56:12 +08:00
Fini 5d015ee570 feat(orchestrator): op-orchestrator crate — S3a design orchestrator
New crate restoring the design orchestrator (TS orchestrator.ts
phases 1-4, single-screen sequential) that the Rust migration
dropped. Pure crate — no winit/casement/agent dependency.

- types.rs: DocSink + LlmClient traits, CallRequest/LlmChunk/
  Progress/RunSummary and friends
- intent.rs: classify_intent (design vs chat)
- plan.rs: OrchestratorPlan parse + heuristic fallback
- parse.rs: LLM output -> canonical PenNode trees
- plan_normalize.rs: single-screen plan normalization
- variables.rs: plan-derived $color-* seed/snapshot/rollback
- prompt.rs: planning + sub-agent prompt assembly via op-ai-skills
- scaffold.rs: single-screen canvas scaffold (InsertSubtree)
- subagent.rs: sequential sub-agent execution
- cleanup.rs: descendant_count + run_cleanup_passes (reusable for S3b)
- run.rs: Orchestrator::run() — 4-phase spine, spec §6 error/abort/
  zero-content semantics
- test_support.rs: VecDocSink + ScriptedLlm test stubs

NOT verified locally: op-orchestrator depends on op-editor-core,
which does not currently build — vendor/jian is pinned to unpushed
commit 80121906 (see prior commit). Unit tests written throughout;
they run once the jian build is restored.

S3a Plan B + Plan C (orchestrator modules).
2026-05-22 22:56:11 +08:00
Fini b1c3136deb feat(editor): EditorCommand::InsertSubtree — nested PenNode subtree insert
Existing EditorCommand variants are leaf-only (BatchInsertItem carries
only kind/name/x/y/w/h/fill_hex). The design orchestrator (S3a) must
apply rich nested designs — frames with children, layout, text — so
add InsertSubtree { nodes: Vec<PenNode>, parent_id }.

cmd_insert_subtree validates the parent is a container (or NONE = page
root), remaps every incoming node id to a fresh editor id via
remap_subtree_ids (so an externally-authored subtree can't collide
with live ids), and appends under the parent. The apply arm wraps it
in a history snapshot so the insert is one undo step.

NOT verified locally: op-editor-core does not currently build —
vendor/jian is pinned to unpushed commit 80121906 whose DesignMd*
types op-editor-core depends on are absent from every available jian.
The 8 InsertSubtree tests in command_subtree_tests.rs run once the
jian build is restored.

S3a Plan A.
2026-05-22 22:56:10 +08:00
Kayshen-X 6e8bc4c5d0 feat(panels): inline export pickers, panel scrolling, and row hover
Export section: the scale / format dropdowns open inline select
popups (1x/2x/3x, PNG/JPEG/WEBP/SVG/PDF) instead of the Export
modal, which is now reached only via File > Export Image. Adds a
full-width Export button; popup rows highlight on hover. Pickers
open upward so they are not clipped at the panel's bottom edge.

PropertyPanel scrolls when its content overflows the viewport,
with the Design / Code tab strip pinned; scroll is clamped on
every paint + hit-test, not just on wheel events.

LayerPanel: the Pages and Layers sections get bounded heights and
independent scroll regions; layer drag/drop is gated to the
visible Layers viewport.

Splits property_panel into property_panel_action / _export and
the host press paths into property_dispatch / scroll modules to
keep every edited file under the 800-line cap.
2026-05-22 22:56:09 +08:00
Kayshen-X 65af173d79 fix(canvas): follow the editor's active page in the layout scene
editor_state_to_layout_scene took active_page_index from the payload, which pen_document_to_payload hardcodes to 0 — so picking a page in the LayerPanel updated the panel but never switched the canvas. Read the live EditorState.ui.active_page_index instead, clamped to the page count.
2026-05-22 22:55:55 +08:00
Kayshen-X a550af4e7f feat(figma): wire .fig binary import into the desktop host
Add op-figma as a dependency and implement FileAction::ImportFigma — an rfd .fig picker parses the binary file via parse_fig_binary and re-seeds EditorState.

run_action now returns a 3-state ActionOutcome instead of a bool: an import returns PathChangedUnsaved so it is treated as unsaved work (close still prompts) while the Git session is rebound to the now-pathless document. mark_document_saved is split so the rebind can run without refreshing the dirty baseline.
2026-05-22 22:55:47 +08:00
Kayshen-X fb5542e778 build(macos): add bundle-macos.sh dev-run wrapper
Wraps the release binary in a minimal `OpenPencil.app`
(Info.plist + icon) so a dev run gets the proper Dock name +
icon — an unbundled binary shows the raw executable name and a
generic icon, and the runtime objc2 fallback in `macos_app.rs`
can't fully override that. Run the binary from inside the bundle
(`OpenPencil.app/Contents/MacOS/openpencil-desktop`) and macOS
picks up the bundle identity.
2026-05-22 18:05:31 +08:00
Kayshen-X b979b97b54 feat(desktop): set the Dock name + icon for the dev binary
Run bare, the non-bundled binary shows in the Dock as the raw
`openpencil-desktop` executable name with a blank icon — no
`Info.plist` to read `CFBundleName` / the icon from.

New `macos_app::apply()` sets both at startup via objc2:
`NSProcessInfo::setProcessName` for the Dock / menu-bar name and
`NSApplication::setApplicationIconImage` (from an embedded
`assets/icon.png`) for the Dock tile. `[package.metadata.bundle]`
also gains `icon`, so a packaged `.app` carries it natively.

objc2-app-kit / -foundation are pinned to the 0.2 line winit /
casement already lock.
2026-05-22 16:13:00 +08:00
Kayshen-X f8358dd566 build(deny): allow BSL-1.0 (Boost) for arboard's Windows deps
cargo-deny failed on `clipboard-win` / `error-code` — pulled in by
`arboard`'s Win32 clipboard path — which are BSL-1.0. The Boost
Software License is permissive + OSI-approved; add it to the
licenses allow-list.
2026-05-22 15:56:06 +08:00
Kayshen-X bb4a989709 feat(desktop): native window chrome + per-agent TopBar icons
Hide the native title bar and let the TopBar own the window
chrome — the Electron `titleBarStyle: 'hidden'` recipe:

- macOS keeps a real `NSWindow` (rounded corners, shadow,
  edge-resize, key-window responsiveness) with the title bar made
  transparent + emptied; the native traffic-light buttons stay,
  pushed down via casement's `with_traffic_light_inset` to centre
  in the 40 px TopBar. Windows / Linux drop decorations and the
  TopBar paints its own close / minimise / maximise dots.
- The TopBar reserves a left inset for the controls; it collapses
  in macOS fullscreen (native lights hide), tracked by a
  per-frame `window.fullscreen()` poll. `window_control_at`
  returns `None` on macOS so a fullscreen click on a left-edge
  app icon can't trigger a window control.
- A press on the TopBar's blank area drags the window.

TopBar chip also gains one brand icon per connected agent + an
`N agent[s] · M MCP` status (new `topbar.agentPlural` across all
15 locales).

Bumps the vendor/casement submodule to 5ad98f1c.
2026-05-22 15:48:21 +08:00
Kayshen-X 4514a6e557 fix(panels): open the colour picker from the hex-row swatch
A click on the colour swatch inside the Fill / Stroke hex input
now opens the picker. Previously only the head-row swatch did, and
the Stroke section had no picker hit-test at all. The head-row
swatch was dropped as a trigger in a follow-up — the hex-row
swatch is the intuitive target. `hit_test_action` runs before the
hex-input focus hit-test, so a swatch click opens the picker
instead of focusing the hex field.
2026-05-22 15:48:00 +08:00
Kayshen-X b1eec55fc2 perf(canvas): batch the dotted grid + honour fill_rect alpha
The infinite-canvas grid painted one `fill_round_rect` per dot —
~1200+ separate skia draw ops every frame, the dominant cost of an
empty-canvas pan / drag. New `RenderBackend::fill_dots` collects
the dot centres and the native backend draws them in a single
`Canvas::draw_points` (round-capped points); other backends keep a
`fill_oval` loop via the default impl.

Also: `NativeBackend::fill_rect` went through `Paint::solid`, which
hardcodes `opacity: 1.0` and dropped the colour's alpha — a
translucent fill (the 12% marquee-selection band) painted fully
opaque. It now carries alpha through `Paint.opacity` like
`stroke_rect` does.
2026-05-22 15:47:49 +08:00