From 11df11dbdba0a0087212e217064fb76c5a25d673 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Fri, 1 May 2026 22:41:10 +0200 Subject: [PATCH] fix: A2UI surfaces --- lib/bds/desktop/shell_live/chat_editor.ex | 2 +- .../shell_live/chat_editor/message_build.ex | 11 ++- priv/ui/live.js | 10 +++ test/bds/desktop/shell_live_test.exs | 71 +++++++++++++++++++ 4 files changed, 87 insertions(+), 7 deletions(-) diff --git a/lib/bds/desktop/shell_live/chat_editor.ex b/lib/bds/desktop/shell_live/chat_editor.ex index eddd6fe..fb48409 100644 --- a/lib/bds/desktop/shell_live/chat_editor.ex +++ b/lib/bds/desktop/shell_live/chat_editor.ex @@ -352,7 +352,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do @spec chat_surface(term()) :: term() def chat_surface(assigns) do ~H""" -
+
<%= surface_icon(@surface.type) %> <%= surface_title(@surface) %> diff --git a/lib/bds/desktop/shell_live/chat_editor/message_build.ex b/lib/bds/desktop/shell_live/chat_editor/message_build.ex index 6f2e39a..3da7b66 100644 --- a/lib/bds/desktop/shell_live/chat_editor/message_build.ex +++ b/lib/bds/desktop/shell_live/chat_editor/message_build.ex @@ -100,7 +100,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do tool_markers: tool_markers, inline_surfaces: ToolSurfaces.build_render_surfaces(tool_markers, message.id, assigns) - |> mark_latest_surface_expanded(assigns), + |> mark_surfaces_expanded(assigns), tool_surfaces: [] } end @@ -125,15 +125,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do } end - defp mark_latest_surface_expanded([], _assigns), do: [] + defp mark_surfaces_expanded([], _assigns), do: [] - defp mark_latest_surface_expanded(surfaces, assigns) do + defp mark_surfaces_expanded(surfaces, assigns) do dismissed = Map.get(assigns, :chat_editor_dismissed_surfaces, MapSet.new()) surfaces |> Enum.reject(&MapSet.member?(dismissed, &1.id)) - |> Enum.with_index() - |> Enum.map(fn {surface, index} -> Map.put(surface, :expanded?, index == length(surfaces) - 1) end) + |> Enum.map(&Map.put(&1, :expanded?, true)) end defp pending_user_message(_messages, nil), do: nil @@ -157,7 +156,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do request |> ToolTracking.tool_markers_from_events() |> ToolSurfaces.build_render_surfaces("streaming-#{conversation_id}", assigns) - |> mark_latest_surface_expanded(assigns) + |> mark_surfaces_expanded(assigns) end defp translated(text, bindings \\ %{}), diff --git a/priv/ui/live.js b/priv/ui/live.js index ffe5109..00a7439 100644 --- a/priv/ui/live.js +++ b/priv/ui/live.js @@ -780,6 +780,14 @@ document.addEventListener("DOMContentLoaded", () => { } }; + this.syncExpandedSurfaces = () => { + this.el + .querySelectorAll(".chat-inline-surface[data-expanded='true']") + .forEach((surface) => { + surface.open = true; + }); + }; + this.handleScroll = () => { if (!this.scrollContainer) { this.stickToBottom = true; @@ -823,12 +831,14 @@ document.addEventListener("DOMContentLoaded", () => { this.el.addEventListener("keydown", this.handleKeyDown); this.syncScrollContainer(); + this.syncExpandedSurfaces(); this.autoResize(); window.requestAnimationFrame(() => this.scrollToBottom(true)); }, updated() { this.syncScrollContainer(); + this.syncExpandedSurfaces(); this.autoResize(); window.requestAnimationFrame(() => this.scrollToBottom()); }, diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs index 59f6e05..383ae86 100644 --- a/test/bds/desktop/shell_live_test.exs +++ b/test/bds/desktop/shell_live_test.exs @@ -2280,6 +2280,77 @@ defmodule BDS.Desktop.ShellLiveTest do refute dismissed_html =~ ~s(data-testid="chat-inline-surface") end + test "chat editor keeps every non-dismissed A2UI surface expanded" do + assert {:ok, conversation} = AI.start_chat(%{title: "Surface Chat", model: "gpt-4.1"}) + + now = Persistence.now_ms() + + Repo.insert!( + BDS.AI.ChatMessage.changeset(%BDS.AI.ChatMessage{}, %{ + conversation_id: conversation.id, + role: :user, + content: "Show two updates", + created_at: now + }) + ) + + Repo.insert!( + BDS.AI.ChatMessage.changeset(%BDS.AI.ChatMessage{}, %{ + conversation_id: conversation.id, + role: :assistant, + content: "Here are the updates.", + tool_calls: + Jason.encode!([ + %{ + "id" => "call-card-old", + "name" => "render_card", + "arguments" => %{ + "title" => "Earlier Missing Data", + "body" => "The first data request needs review." + } + }, + %{ + "id" => "call-card-new", + "name" => "render_card", + "arguments" => %{ + "title" => "Latest Missing Data", + "body" => "The second data request needs review." + } + } + ]), + created_at: now + 1 + }) + ) + + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + html = + render_click(view, "pin_sidebar_item", %{ + "route" => "chat", + "id" => conversation.id, + "title" => conversation.title, + "subtitle" => conversation.model || "chat" + }) + + assert length(:binary.matches(html, ~s(data-testid="chat-inline-surface"))) == 2 + assert length(:binary.matches(html, "data-expanded")) == 2 + assert html =~ "Earlier Missing Data" + assert html =~ "The first data request needs review." + assert html =~ "Latest Missing Data" + assert html =~ "The second data request needs review." + end + + test "chat editor hook reopens server-expanded A2UI surfaces after patches" do + live_js = File.read!(Path.expand("../../../priv/ui/live.js", __DIR__)) + chat_editor = File.read!(Path.expand("../../../lib/bds/desktop/shell_live/chat_editor.ex", __DIR__)) + + assert chat_editor =~ "data-expanded={Map.get(@surface, :expanded?, false)}" + assert live_js =~ "this.syncExpandedSurfaces = () =>" + assert live_js =~ "querySelectorAll(\".chat-inline-surface[data-expanded='true']\")" + assert live_js =~ "surface.open = true;" + assert live_js =~ "this.syncExpandedSurfaces();" + end + test "chat editor folds tool-only assistant steps into the final assistant answer" do assert {:ok, conversation} = AI.start_chat(%{title: "Tool Chat", model: "gpt-4.1"})