diff --git a/ai/transcript.ml b/ai/transcript.ml index cc8b34f7..6fab4a4f 100644 --- a/ai/transcript.ml +++ b/ai/transcript.ml @@ -235,6 +235,11 @@ let rendered_body cfg fallback item_s = | Some view -> view | None -> fallback item_s +let content_row cfg body = + row ~main:`center ~grow:1.0 ~min_width:0 + [ column ~grow:1.0 ~max_width:cfg.content_width ~min_width:0 + ~cross:`stretch ~padding_horizontal:8 ~padding_vertical:4 [ body ] ] + let grouped_item cfg (item_s : item Signal.signal) : t = scoped (fun _context -> let item = sample item_s in @@ -279,7 +284,7 @@ let tool_group cfg (row_s : row Signal.signal) : t = in Signal.set open_ (Some (not current)) in - accordion + content_row cfg (accordion ~text:(reactive (fun (items : item list) (active : bool) -> let n = List.length items in @@ -292,7 +297,7 @@ let tool_group cfg (row_s : row Signal.signal) : t = ~accessibility_identifier:"lui-ai-tool-group" [ column ~cross:`stretch ~gap:6 [ keyed ~source:items_s ~key:(fun (i : item) -> i.id) - ~cmp:String.compare ~mount:(grouped_item cfg) ] ]) + ~cmp:String.compare ~mount:(grouped_item cfg) ] ])) let builtin_body cfg context (item_s : item Signal.signal) : t = ignore context; @@ -315,9 +320,7 @@ let item_row_of cfg context (item_s : item Signal.signal) : t = ?style_class: (match cfg.item_class item with "" -> None | s -> Some s) ~data_attrs:(reactive cfg.item_data_attrs item_s) - [ row ~main:`center ~grow:1.0 - [ column ~grow:1.0 ~max_width:cfg.content_width ~cross:`stretch - ~padding_horizontal:8 ~padding_vertical:4 [ body ] ] ] + [ content_row cfg body ] let row_of cfg (row_s : row Signal.signal) : t = scoped (fun context -> diff --git a/docs/agent-guide/proposed/bugfix/2026-10-19-bounded-tool-group-layout.md b/docs/agent-guide/proposed/bugfix/2026-10-19-bounded-tool-group-layout.md new file mode 100644 index 00000000..e7258c51 --- /dev/null +++ b/docs/agent-guide/proposed/bugfix/2026-10-19-bounded-tool-group-layout.md @@ -0,0 +1,57 @@ +# Bounded Tool Group Layout + +## Problem + +Mapre's actual browser consumer shows a completed tool-group summary spanning +1160px while ordinary message content is bounded to 768px. The transcript's +Tool_group path bypasses the centered content column used by Single rows. + +## Proposal + +Give grouped tools the same centered bounded content surface as messages while +keeping the outer scroll viewport full width. Share the static layout helper +between Single and Tool_group, preserve host renderers, keyed child identity, +reactive metadata and disclosure state. Do not add app-specific CSS, vendor a +copy, alter Dune files or add another scroll owner. + +## Alternatives considered + +### Constrain the scroll viewport + +Rejected because it recreates ignored wheel input in blank conversation gutters. + +### Override the consumer with layout CSS + +Rejected because the generic transcript already owns content_width and must +apply it consistently across row kinds and platforms. + +## Acceptance criteria + +- Native mounted grouped-only transcripts respect an explicit non-default + content_width and include centered content layout; the case fails first. +- Existing host-rendering, streaming, keyed lifetime, disclosure and transcript + tests pass. The outer viewport still grows across the conversation pane. +- The actual Mapre browser consumer has bounded centered activity, accessible + disclosure and stable host details on desktop and mobile. +- Submit the upstream PR and consume an installed reviewed package; no vendoring. + +## Risks + +- Group summaries gain the same horizontal/vertical padding as message content; + disclosure height changes must retain the host's scroll anchoring behavior. + +## Questions + +None. This completes the existing transcript layout contract and the user's +explicit request to fix shared LUI issues upstream before consumer integration. + +## Verification + +The new grouped-only mounted native case fails before the fix because no +centered/bounded content node exists. It now passes with a non-default 560px +content_width; all 25 AI cases and all 148 native test_lui cases pass. Melange +and install targets compile. The helper is shared with ordinary message rows; +no scroll viewport, event handler, public signature or dependency changes. +The consumer's desktop browser first reports an unbounded 1160px summary. +Final installed-package consumer acceptance remains pending; this decision +stays proposed until that actual browser proof is complete. diff --git a/test/test_lui_ai.ml b/test/test_lui_ai.ml index 51dba2fb..b7130ed5 100644 --- a/test/test_lui_ai.ml +++ b/test/test_lui_ai.ml @@ -607,6 +607,20 @@ let test_bounded_layout () = ignore bounded; drive_dispose s +let test_group_bounded_layout () = + let view _ctx source _send = + Lui_ai.Transcript.transcript ~content_width:560 ~messages:source () in + let s = drive_mount + ~initial:[ tool_item "t1" `completed; + tool_item "t2" `completed ] ~view in + ignore (find_node s (fun n -> + n.Drive.Model.kind = "row" + && node_prop n "main" = Some (StringValue "center"))); + ignore (find_node s (fun n -> + n.Drive.Model.kind = "column" + && node_prop n "max-width" = Some (IntValue 560))); + drive_dispose s + let test_container_data_attrs_signal () = let attrs_ref = ref None in let view _ctx model_s _send = @@ -653,6 +667,7 @@ let tests = [ Alcotest.test_case "grouped tail key" `Quick test_grouped_tail_key; Alcotest.test_case "grouped pin" `Quick test_grouped_pin; Alcotest.test_case "bounded layout" `Quick test_bounded_layout; + Alcotest.test_case "group bounded layout" `Quick test_group_bounded_layout; Alcotest.test_case "container attrs signal" `Quick test_container_data_attrs_signal; Alcotest.test_case "streaming updates same row" `Quick