fix: more work on A2UI

This commit is contained in:
2026-05-01 22:22:59 +02:00
parent 391a7f216f
commit a17c549817
12 changed files with 228 additions and 59 deletions

View File

@@ -135,6 +135,7 @@ defmodule BDS.Desktop.ShellLive do
|> assign(:chat_editor_request_refs, %{}) |> assign(:chat_editor_request_refs, %{})
|> assign(:chat_editor_surface_data, %{}) |> assign(:chat_editor_surface_data, %{})
|> assign(:chat_editor_surface_tabs, %{}) |> assign(:chat_editor_surface_tabs, %{})
|> assign(:chat_editor_dismissed_surfaces, MapSet.new())
|> assign(:chat_editor_action_errors, %{}) |> assign(:chat_editor_action_errors, %{})
|> assign(:import_editor_analysis_states, %{}) |> assign(:import_editor_analysis_states, %{})
|> assign(:import_editor_analysis_task_refs, %{}) |> assign(:import_editor_analysis_task_refs, %{})
@@ -948,6 +949,10 @@ defmodule BDS.Desktop.ShellLive do
ChatEditor.select_surface_tab(socket, surface_id, parse_integer(index), &reload_shell/2)} ChatEditor.select_surface_tab(socket, surface_id, parse_integer(index), &reload_shell/2)}
end end
def handle_event("dismiss_chat_surface", %{"surface-id" => surface_id}, socket) do
{:noreply, ChatEditor.dismiss_surface(socket, surface_id, &reload_shell/2)}
end
def handle_event("chat_surface_action", params, socket) do def handle_event("chat_surface_action", params, socket) do
{:noreply, {:noreply,
ChatSurface.handle_action(socket, params, %{ ChatSurface.handle_action(socket, params, %{

View File

@@ -59,6 +59,16 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|> reload.(socket.assigns.workbench) |> reload.(socket.assigns.workbench)
end end
@spec dismiss_surface(term(), term(), term()) :: term()
def dismiss_surface(socket, surface_id, reload) when is_binary(surface_id) do
socket
|> assign(
:chat_editor_dismissed_surfaces,
MapSet.put(socket.assigns.chat_editor_dismissed_surfaces, surface_id)
)
|> reload.(socket.assigns.workbench)
end
@spec current_surface_data(term(), term()) :: term() @spec current_surface_data(term(), term()) :: term()
def current_surface_data(socket, surface_id) when is_binary(surface_id) do def current_surface_data(socket, surface_id) when is_binary(surface_id) do
Map.get(socket.assigns.chat_editor_surface_data, surface_id, %{}) Map.get(socket.assigns.chat_editor_surface_data, surface_id, %{})
@@ -314,13 +324,23 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
<%= if @markers != [] do %> <%= if @markers != [] do %>
<div class="chat-tool-markers"> <div class="chat-tool-markers">
<%= for marker <- @markers do %> <%= for marker <- @markers do %>
<div class={["chat-tool-marker", if(marker.complete?, do: "completed", else: "pending")]} data-testid="chat-tool-marker"> <details class={["chat-tool-marker", if(marker.complete?, do: "completed", else: "pending")]} data-testid="chat-tool-marker">
<span class="chat-tool-marker-icon"><%= if marker.complete?, do: "✓", else: "●" %></span> <summary>
<span class="chat-tool-marker-name"><%= marker.name %></span> <span class="chat-tool-marker-icon"><%= if marker.complete?, do: "✓", else: "●" %></span>
<%= if marker.args_preview not in [nil, ""] do %> <span class="chat-tool-marker-name"><%= marker.name %></span>
<span class="chat-tool-marker-args">(<%= marker.args_preview %>)</span> <%= if marker.args_preview not in [nil, ""] do %>
<% end %> <span class="chat-tool-marker-args">(<%= marker.args_preview %>)</span>
</div> <% end %>
</summary>
<div class="chat-tool-marker-details" data-testid="chat-tool-marker-details">
<div class="chat-tool-marker-detail-label"><%= translated("chat.toolArguments") %></div>
<pre><%= Jason.encode!(marker.arguments || %{}, pretty: true) %></pre>
<%= if marker.result not in [nil, ""] do %>
<div class="chat-tool-marker-detail-label"><%= translated("chat.toolResult") %></div>
<pre><%= marker.result %></pre>
<% end %>
</div>
</details>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
@@ -332,7 +352,13 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
@spec chat_surface(term()) :: term() @spec chat_surface(term()) :: term()
def chat_surface(assigns) do def chat_surface(assigns) do
~H""" ~H"""
<article class={["chat-inline-surface", "chat-inline-surface-#{@surface.type}"]} data-testid="chat-inline-surface"> <details id={@surface.id} class={["chat-inline-surface", "chat-inline-surface-#{@surface.type}"]} data-testid="chat-inline-surface" open={Map.get(@surface, :expanded?, false)}>
<summary class="chat-inline-surface-header">
<span class="chat-inline-surface-icon"><%= surface_icon(@surface.type) %></span>
<span class="chat-inline-surface-title"><%= surface_title(@surface) %></span>
<button class="chat-inline-surface-dismiss" type="button" phx-click="dismiss_chat_surface" phx-value-surface-id={@surface.id} aria-label={translated("chat.dismissSurface")} data-testid="chat-inline-surface-dismiss">×</button>
</summary>
<div class="chat-inline-surface-body">
<%= case @surface.type do %> <%= case @surface.type do %>
<% "card" -> %> <% "card" -> %>
<div class="chat-surface-card"> <div class="chat-surface-card">
@@ -526,10 +552,28 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
<% _other -> %> <% _other -> %>
<pre class="chat-tool-surface-json"><%= Jason.encode!(@surface.raw || %{}, pretty: true) %></pre> <pre class="chat-tool-surface-json"><%= Jason.encode!(@surface.raw || %{}, pretty: true) %></pre>
<% end %> <% end %>
</article> </div>
</details>
""" """
end end
defp surface_icon("chart"), do: ""
defp surface_icon("table"), do: ""
defp surface_icon("form"), do: ""
defp surface_icon("card"), do: ""
defp surface_icon("metric"), do: "#"
defp surface_icon("list"), do: ""
defp surface_icon("tabs"), do: ""
defp surface_icon(_type), do: ""
defp surface_title(surface) do
cond do
present?(Map.get(surface, :title)) -> Map.get(surface, :title)
present?(Map.get(surface, :label)) -> Map.get(surface, :label)
true -> surface.type |> to_string() |> String.capitalize()
end
end
# ── Private helpers ─────────────────────────────────────────────────────── # ── Private helpers ───────────────────────────────────────────────────────
defp update_request(socket, conversation_id, updater, reload) do defp update_request(socket, conversation_id, updater, reload) do

View File

@@ -31,6 +31,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
is_streaming: not is_nil(request), is_streaming: not is_nil(request),
streaming_content: streaming_content(request), streaming_content: streaming_content(request),
streaming_tool_markers: ToolTracking.tool_markers_from_events(request), streaming_tool_markers: ToolTracking.tool_markers_from_events(request),
streaming_inline_surfaces: streaming_inline_surfaces(conversation.id, request, assigns),
offline?: Map.get(assigns, :offline_mode, true), offline?: Map.get(assigns, :offline_mode, true),
needs_api_key?: ModelSelection.needs_api_key?(Map.get(assigns, :offline_mode, true)), needs_api_key?: ModelSelection.needs_api_key?(Map.get(assigns, :offline_mode, true)),
action_error: Map.get(assigns.chat_editor_action_errors, conversation.id), action_error: Map.get(assigns.chat_editor_action_errors, conversation.id),
@@ -49,7 +50,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
case message.role do case message.role do
:tool -> :tool ->
if current_entry && current_entry.role == :assistant do if current_entry && current_entry.role == :assistant do
{entries, append_tool_surface(current_entry, message), turn_index} {entries, append_tool_result(current_entry, message), turn_index}
else else
{entries, current_entry, turn_index} {entries, current_entry, turn_index}
end end
@@ -95,18 +96,15 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
content: message.content || "", content: message.content || "",
turn_index: turn_index, turn_index: turn_index,
tool_markers: tool_markers, tool_markers: tool_markers,
inline_surfaces: ToolSurfaces.build_render_surfaces(tool_markers, message.id, assigns), inline_surfaces:
ToolSurfaces.build_render_surfaces(tool_markers, message.id, assigns)
|> mark_latest_surface_expanded(assigns),
tool_surfaces: [] tool_surfaces: []
} }
end end
defp append_tool_surface(entry, message) do defp append_tool_result(entry, message) do
entry = ToolTracking.mark_tool_call_completed(entry, message.tool_call_id) ToolTracking.mark_tool_call_completed(entry, message.tool_call_id, message.content)
case ToolSurfaces.normalize_tool_surface(message.content) do
nil -> entry
surface -> update_in(entry.tool_surfaces, &(&1 ++ [surface]))
end
end end
defp tool_only_assistant_entry?(%{role: :assistant, content: content} = entry) do defp tool_only_assistant_entry?(%{role: :assistant, content: content} = entry) do
@@ -125,6 +123,17 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
} }
end end
defp mark_latest_surface_expanded([], _assigns), do: []
defp mark_latest_surface_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)
end
defp pending_user_message(_messages, nil), do: nil defp pending_user_message(_messages, nil), do: nil
defp pending_user_message(messages, %{message: message}) when is_binary(message) do defp pending_user_message(messages, %{message: message}) when is_binary(message) do
@@ -140,6 +149,15 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
defp streaming_content(%{content: content}) when is_binary(content), do: content defp streaming_content(%{content: content}) when is_binary(content), do: content
defp streaming_content(_request), do: "" defp streaming_content(_request), do: ""
defp streaming_inline_surfaces(_conversation_id, nil, _assigns), do: []
defp streaming_inline_surfaces(conversation_id, request, assigns) do
request
|> ToolTracking.tool_markers_from_events()
|> ToolSurfaces.build_render_surfaces("streaming-#{conversation_id}", assigns)
|> mark_latest_surface_expanded(assigns)
end
defp translated(text, bindings \\ %{}), defp translated(text, bindings \\ %{}),
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
end end

View File

@@ -22,6 +22,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
name: tool_call_name(tool_call), name: tool_call_name(tool_call),
arguments: arguments, arguments: arguments,
args_preview: tool_arguments_preview(arguments), args_preview: tool_arguments_preview(arguments),
result: nil,
complete?: false complete?: false
} }
end) end)
@@ -39,11 +40,19 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
@spec tool_arguments_preview(term()) :: term() @spec tool_arguments_preview(term()) :: term()
def tool_arguments_preview(_arguments), do: "" def tool_arguments_preview(_arguments), do: ""
@spec mark_tool_call_completed(term(), term()) :: term()
def mark_tool_call_completed(entry, tool_call_id) when is_binary(tool_call_id) do def mark_tool_call_completed(entry, tool_call_id) when is_binary(tool_call_id) do
mark_tool_call_completed(entry, tool_call_id, nil)
end
def mark_tool_call_completed(entry, _tool_call_id), do: entry
@spec mark_tool_call_completed(term(), term(), term()) :: term()
def mark_tool_call_completed(entry, tool_call_id, result) when is_binary(tool_call_id) do
update_in(entry.tool_markers, fn markers -> update_in(entry.tool_markers, fn markers ->
Enum.map(markers, fn marker -> Enum.map(markers, fn marker ->
if marker.id == tool_call_id do if marker.id == tool_call_id do
%{marker | complete?: true} %{marker | complete?: true, result: result}
else else
marker marker
end end
@@ -51,8 +60,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
end) end)
end end
@spec mark_tool_call_completed(term(), term()) :: term() def mark_tool_call_completed(entry, _tool_call_id, _result), do: entry
def mark_tool_call_completed(entry, _tool_call_id), do: entry
@spec tool_markers_from_events(term()) :: term() @spec tool_markers_from_events(term()) :: term()
def tool_markers_from_events(nil), do: [] def tool_markers_from_events(nil), do: []
@@ -68,6 +76,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolTracking do
name: event.name, name: event.name,
arguments: event.arguments, arguments: event.arguments,
args_preview: tool_arguments_preview(event.arguments || %{}), args_preview: tool_arguments_preview(event.arguments || %{}),
result: nil,
complete?: false complete?: false
} }
] ]

View File

@@ -109,40 +109,6 @@
<.chat_surface surface={surface} /> <.chat_surface surface={surface} />
<% end %> <% end %>
<%= for surface <- message.tool_surfaces do %>
<article class="chat-tool-surface" data-testid="chat-tool-surface">
<%= if surface.title do %>
<h3><%= surface.title %></h3>
<% end %>
<%= case tool_surface_type(surface) do %>
<% "table" -> %>
<div class="chat-tool-surface-table-wrap">
<table class="chat-tool-surface-table">
<thead>
<tr>
<%= for column <- surface.columns do %>
<th><%= column %></th>
<% end %>
</tr>
</thead>
<tbody>
<%= for row <- surface.rows do %>
<tr>
<%= for value <- row do %>
<td><%= value %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
<% _other -> %>
<pre class="chat-tool-surface-json"><%= Jason.encode!(surface.data, pretty: true) %></pre>
<% end %>
</article>
<% end %>
<% end %> <% end %>
<%= if @chat_editor.is_streaming and (@chat_editor.streaming_content != "" or @chat_editor.streaming_tool_markers != []) do %> <%= if @chat_editor.is_streaming and (@chat_editor.streaming_content != "" or @chat_editor.streaming_tool_markers != []) do %>
@@ -160,6 +126,10 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<%= for surface <- @chat_editor.streaming_inline_surfaces do %>
<.chat_surface surface={surface} />
<% end %>
<% end %> <% end %>
<%= if @chat_editor.is_streaming and @chat_editor.streaming_content == "" and @chat_editor.streaming_tool_markers == [] do %> <%= if @chat_editor.is_streaming and @chat_editor.streaming_content == "" and @chat_editor.streaming_tool_markers == [] do %>

View File

@@ -109,6 +109,9 @@
"chat.role.assistant": "Assistent", "chat.role.assistant": "Assistent",
"chat.inputPlaceholder": "Nachricht eingeben...", "chat.inputPlaceholder": "Nachricht eingeben...",
"chat.stop": "Stopp", "chat.stop": "Stopp",
"chat.toolArguments": "Argumente",
"chat.toolResult": "Ergebnis",
"chat.dismissSurface": "Ansicht schließen",
"chat.cancelledSuffix": "(abgebrochen)", "chat.cancelledSuffix": "(abgebrochen)",
"gitDiff.changedFiles": "Geänderte Dateien", "gitDiff.changedFiles": "Geänderte Dateien",
"sidebar.tags": "Schlagwörter", "sidebar.tags": "Schlagwörter",

View File

@@ -109,6 +109,9 @@
"chat.role.assistant": "Assistant", "chat.role.assistant": "Assistant",
"chat.inputPlaceholder": "Type a message...", "chat.inputPlaceholder": "Type a message...",
"chat.stop": "Stop", "chat.stop": "Stop",
"chat.toolArguments": "Arguments",
"chat.toolResult": "Result",
"chat.dismissSurface": "Dismiss surface",
"chat.cancelledSuffix": "(cancelled)", "chat.cancelledSuffix": "(cancelled)",
"gitDiff.changedFiles": "Changed files", "gitDiff.changedFiles": "Changed files",
"sidebar.tags": "Tags", "sidebar.tags": "Tags",

View File

@@ -109,6 +109,9 @@
"chat.role.assistant": "Asistente", "chat.role.assistant": "Asistente",
"chat.inputPlaceholder": "Escribe un mensaje...", "chat.inputPlaceholder": "Escribe un mensaje...",
"chat.stop": "Detener", "chat.stop": "Detener",
"chat.toolArguments": "Argumentos",
"chat.toolResult": "Resultado",
"chat.dismissSurface": "Cerrar superficie",
"chat.cancelledSuffix": "(cancelado)", "chat.cancelledSuffix": "(cancelado)",
"gitDiff.changedFiles": "Archivos modificados", "gitDiff.changedFiles": "Archivos modificados",
"sidebar.tags": "Etiquetas", "sidebar.tags": "Etiquetas",

View File

@@ -109,6 +109,9 @@
"chat.role.assistant": "Assistant IA", "chat.role.assistant": "Assistant IA",
"chat.inputPlaceholder": "Saisissez un message...", "chat.inputPlaceholder": "Saisissez un message...",
"chat.stop": "Arrêter", "chat.stop": "Arrêter",
"chat.toolArguments": "Arguments",
"chat.toolResult": "Résultat",
"chat.dismissSurface": "Fermer la surface",
"chat.cancelledSuffix": "(annulé)", "chat.cancelledSuffix": "(annulé)",
"gitDiff.changedFiles": "Fichiers modifiés", "gitDiff.changedFiles": "Fichiers modifiés",
"sidebar.tags": "Étiquettes", "sidebar.tags": "Étiquettes",

View File

@@ -109,6 +109,9 @@
"chat.role.assistant": "Assistente", "chat.role.assistant": "Assistente",
"chat.inputPlaceholder": "Scrivi un messaggio...", "chat.inputPlaceholder": "Scrivi un messaggio...",
"chat.stop": "Ferma", "chat.stop": "Ferma",
"chat.toolArguments": "Argomenti",
"chat.toolResult": "Risultato",
"chat.dismissSurface": "Chiudi superficie",
"chat.cancelledSuffix": "(annullato)", "chat.cancelledSuffix": "(annullato)",
"gitDiff.changedFiles": "File modificati", "gitDiff.changedFiles": "File modificati",
"sidebar.tags": "Tag", "sidebar.tags": "Tag",

View File

@@ -5558,13 +5558,45 @@ button svg * {
} }
.chat-tool-marker { .chat-tool-marker {
font-size: 12px;
color: var(--vscode-descriptionForeground, inherit);
}
.chat-tool-marker summary {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
font-size: 12px; cursor: pointer;
list-style: none;
}
.chat-tool-marker summary::-webkit-details-marker {
display: none;
}
.chat-tool-marker-details {
margin: 6px 0 2px 20px;
padding: 8px;
border: 1px solid var(--vscode-editorGroup-border, var(--line, #3c3c3c));
border-radius: 6px;
background-color: var(--vscode-editor-background, rgba(0, 0, 0, 0.18));
}
.chat-tool-marker-detail-label {
margin: 0 0 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: var(--vscode-descriptionForeground, inherit); color: var(--vscode-descriptionForeground, inherit);
} }
.chat-tool-marker-details pre {
margin: 0 0 8px;
white-space: pre-wrap;
overflow-wrap: anywhere;
font: 11px/1.45 "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
}
.chat-tool-marker.completed .chat-tool-marker-icon { .chat-tool-marker.completed .chat-tool-marker-icon {
color: var(--vscode-testing-iconPassed, #89d185); color: var(--vscode-testing-iconPassed, #89d185);
} }
@@ -5581,13 +5613,60 @@ button svg * {
.chat-tool-surface { .chat-tool-surface {
width: min(720px, calc(100% - 44px)); width: min(720px, calc(100% - 44px));
margin-left: 44px; margin-left: 44px;
padding: 14px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid var(--vscode-editorGroup-border, var(--line, #3c3c3c)); border: 1px solid var(--vscode-editorGroup-border, var(--line, #3c3c3c));
border-radius: 12px; border-radius: 12px;
background-color: var(--vscode-sideBar-background, var(--panel-2, #252526)); background-color: var(--vscode-sideBar-background, var(--panel-2, #252526));
} }
.chat-inline-surface {
overflow: hidden;
}
.chat-inline-surface-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
list-style: none;
background-color: var(--vscode-textBlockQuote-background, rgba(127, 127, 127, 0.1));
}
.chat-inline-surface-header::-webkit-details-marker {
display: none;
}
.chat-inline-surface-title {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
font-weight: 600;
}
.chat-inline-surface-dismiss {
border: 0;
border-radius: 4px;
padding: 2px 7px;
background: transparent;
color: var(--vscode-descriptionForeground, inherit);
cursor: pointer;
font-size: 15px;
line-height: 1.2;
}
.chat-inline-surface-dismiss:hover {
background-color: var(--vscode-toolbar-hoverBackground, rgba(255, 255, 255, 0.08));
color: var(--vscode-foreground, inherit);
}
.chat-inline-surface-body {
padding: 14px;
}
.chat-inline-surface h3, .chat-inline-surface h3,
.chat-tool-surface h3 { .chat-tool-surface h3 {
margin: 0 0 12px; margin: 0 0 12px;

View File

@@ -2149,7 +2149,7 @@ defmodule BDS.Desktop.ShellLiveTest do
assert css =~ "position: static;" assert css =~ "position: static;"
end end
test "chat editor renders legacy model controls, tool markers, and structured tool surfaces" do test "chat editor renders legacy model controls, collapsed tool pills, and dismissible A2UI surfaces" do
assert {:ok, conversation} = AI.start_chat(%{title: "Editor Chat", model: "gpt-4.1"}) assert {:ok, conversation} = AI.start_chat(%{title: "Editor Chat", model: "gpt-4.1"})
now = Persistence.now_ms() now = Persistence.now_ms()
@@ -2209,11 +2209,21 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-testid="chat-model-selector-button") assert html =~ ~s(data-testid="chat-model-selector-button")
assert html =~ "gpt-4.1" assert html =~ "gpt-4.1"
assert html =~ ~s(data-testid="chat-tool-marker") assert html =~ ~s(data-testid="chat-tool-marker")
assert html =~ ~s(data-testid="chat-tool-marker-details")
assert html =~ "render_table" assert html =~ "render_table"
assert html =~ ~s(data-testid="chat-tool-surface") refute html =~ ~s(data-testid="chat-tool-surface")
assert html =~ ~s(data-testid="chat-inline-surface")
assert html =~ ~s(data-testid="chat-inline-surface-dismiss")
assert html =~ "Blog Stats" assert html =~ "Blog Stats"
assert html =~ "Metric" assert html =~ "Metric"
assert html =~ "Posts" assert html =~ "Posts"
dismissed_html =
render_click(view, "dismiss_chat_surface", %{
"surface-id" => Regex.run(~r/id="([^"]+-surface-0)"/, html) |> Enum.at(1)
})
refute dismissed_html =~ ~s(data-testid="chat-inline-surface")
end end
test "chat editor folds tool-only assistant steps into the final assistant answer" do test "chat editor folds tool-only assistant steps into the final assistant answer" do
@@ -2647,6 +2657,25 @@ defmodule BDS.Desktop.ShellLiveTest do
assert assistant_index < user_index assert assistant_index < user_index
assert html =~ ~r/<textarea[^>]*class="chat-input chat-surface-input"[^>]*disabled/ assert html =~ ~r/<textarea[^>]*class="chat-input chat-surface-input"[^>]*disabled/
send(view.pid, {
:chat_tool_call,
conversation.id,
%{
id: "call-streaming-chart",
name: "render_chart",
arguments: %{
"title" => "Streaming Chart",
"chartType" => "bar",
"series" => [%{"label" => "Posts", "value" => 3}]
}
}
})
html = render(view)
assert html =~ ~s(data-testid="chat-streaming-message")
assert html =~ ~s(data-testid="chat-inline-surface")
assert html =~ "Streaming Chart"
html = html =
view view
|> element("[data-testid='chat-abort-button']") |> element("[data-testid='chat-abort-button']")