From 8e8a2e2cd27a9f2d895a311a4a8ba7c70215c4db Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 26 Apr 2026 09:48:53 +0200 Subject: [PATCH] feat: preliminary work on assistant --- lib/bds/desktop/shell_live.ex | 49 ++++++++ lib/bds/desktop/shell_live/index.html.heex | 78 +++++++++++-- priv/i18n/locales/de.json | 6 +- priv/i18n/locales/en.json | 6 +- priv/i18n/locales/es.json | 6 +- priv/i18n/locales/fr.json | 6 +- priv/i18n/locales/it.json | 6 +- priv/ui/app.css | 128 +++++++++++++++++++++ test/bds/desktop/shell_live_test.exs | 26 +++++ test/bds/ui/shell_test.exs | 15 +++ 10 files changed, 313 insertions(+), 13 deletions(-) diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex index 122dd49..ea16502 100644 --- a/lib/bds/desktop/shell_live.ex +++ b/lib/bds/desktop/shell_live.ex @@ -47,6 +47,8 @@ defmodule BDS.Desktop.ShellLive do |> assign(:page_language, ShellData.ui_language()) |> assign(:client_shortcuts, Commands.client_shortcuts()) |> assign(:offline_mode, true) + |> assign(:assistant_prompt, "") + |> assign(:assistant_messages, []) |> assign(:is_mac_ui, mac_ui?()) |> assign(:menu_groups, titlebar_menu_groups()) |> assign(:titlebar_menu_group, nil) @@ -275,6 +277,25 @@ defmodule BDS.Desktop.ShellLive do {:noreply, reload_shell(socket, socket.assigns.workbench)} end + def handle_event("update_assistant_prompt", %{"assistant" => %{"prompt" => prompt}}, socket) do + {:noreply, assign(socket, :assistant_prompt, prompt)} + end + + def handle_event("submit_assistant_prompt", %{"assistant" => %{"prompt" => prompt}}, socket) do + prompt = prompt |> to_string() |> String.trim() + + socket = + if prompt == "" do + assign(socket, :assistant_prompt, "") + else + socket + |> assign(:assistant_prompt, "") + |> assign(:assistant_messages, socket.assigns.assistant_messages ++ assistant_turn(prompt, socket)) + end + + {:noreply, socket} + end + def handle_event("open_tasks_panel", _params, socket) do workbench = socket.assigns.workbench @@ -1506,6 +1527,34 @@ defmodule BDS.Desktop.ShellLive do defp tab_icon_id(%{type: :style}), do: "settings" defp tab_icon_id(%{type: type}), do: Atom.to_string(type) + defp assistant_turn(prompt, socket) do + [ + %{role: "user", content: prompt}, + %{role: "assistant", content: assistant_reply(socket)} + ] + end + + defp assistant_reply(socket) do + if socket.assigns.offline_mode do + ShellData.translate("Automatic AI actions stay gated by airplane mode.", %{}, socket.assigns.page_language) + else + ShellData.translate( + "The assistant sidebar chat surface is ready, but model execution is not connected yet.", + %{}, + socket.assigns.page_language + ) + end + end + + defp assistant_project_name(nil), do: translated("Projects") + defp assistant_project_name(project), do: project.name + + defp assistant_message_label("assistant"), do: translated("Assistant") + defp assistant_message_label("user"), do: translated("You") + defp assistant_message_label(_role), do: translated("Assistant") + + defp assistant_message_testid(role), do: "assistant-message-#{role}" + defp media_thumbnail_glyph(mime_type) do case String.split(to_string(mime_type || ""), "/", parts: 2) do ["image", _rest] -> "IMG" diff --git a/lib/bds/desktop/shell_live/index.html.heex b/lib/bds/desktop/shell_live/index.html.heex index ffb8659..fdaa9d8 100644 --- a/lib/bds/desktop/shell_live/index.html.heex +++ b/lib/bds/desktop/shell_live/index.html.heex @@ -416,15 +416,77 @@ >
diff --git a/priv/i18n/locales/de.json b/priv/i18n/locales/de.json index 6010149..936dfd6 100644 --- a/priv/i18n/locales/de.json +++ b/priv/i18n/locales/de.json @@ -221,5 +221,9 @@ "View": "Ansicht", "Welcome to bDS2": "Willkommen bei bDS2", "Workbench Notes": "Workbench-Hinweise", - "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "Die Integration des Arbeitsverzeichnisses ist in der Shell noch nicht verdrahtet, aber der Tab ist auswählbar und bereit für Befehlsausgaben." + "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "Die Integration des Arbeitsverzeichnisses ist in der Shell noch nicht verdrahtet, aber der Tab ist auswählbar und bereit für Befehlsausgaben.", + "Ask the assistant about the active project or editor.": "Frage den Assistenten zum aktiven Projekt oder Editor.", + "Start chat": "Chat starten", + "You": "Du", + "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "Die Chat-Oberfläche der Assistenten-Seitenleiste ist bereit, aber die Modellausführung ist noch nicht verbunden." } \ No newline at end of file diff --git a/priv/i18n/locales/en.json b/priv/i18n/locales/en.json index 4705955..92fa5bd 100644 --- a/priv/i18n/locales/en.json +++ b/priv/i18n/locales/en.json @@ -221,5 +221,9 @@ "View": "View", "Welcome to bDS2": "Welcome to bDS2", "Workbench Notes": "Workbench Notes", - "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output." + "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.", + "Ask the assistant about the active project or editor.": "Ask the assistant about the active project or editor.", + "Start chat": "Start chat", + "You": "You", + "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "The assistant sidebar chat surface is ready, but model execution is not connected yet." } \ No newline at end of file diff --git a/priv/i18n/locales/es.json b/priv/i18n/locales/es.json index e9519b0..a938187 100644 --- a/priv/i18n/locales/es.json +++ b/priv/i18n/locales/es.json @@ -221,5 +221,9 @@ "View": "Ver", "Welcome to bDS2": "Bienvenido a bDS2", "Workbench Notes": "Notas del área de trabajo", - "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "La integración del árbol de trabajo aún no está conectada en el shell, pero la pestaña es seleccionable y está lista para la salida de comandos." + "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "La integración del árbol de trabajo aún no está conectada en el shell, pero la pestaña es seleccionable y está lista para la salida de comandos.", + "Ask the assistant about the active project or editor.": "Pregunta al asistente sobre el proyecto o editor activo.", + "Start chat": "Iniciar chat", + "You": "Tú", + "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "La superficie de chat de la barra lateral del asistente está lista, pero la ejecución del modelo aún no está conectada." } \ No newline at end of file diff --git a/priv/i18n/locales/fr.json b/priv/i18n/locales/fr.json index 5ca50c8..ee53a7e 100644 --- a/priv/i18n/locales/fr.json +++ b/priv/i18n/locales/fr.json @@ -221,5 +221,9 @@ "View": "Affichage", "Welcome to bDS2": "Bienvenue dans bDS2", "Workbench Notes": "Notes d’atelier", - "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "L’intégration de l’arbre de travail n’est pas encore câblée dans le shell, mais l’onglet est sélectionnable et prêt pour la sortie des commandes." + "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "L’intégration de l’arbre de travail n’est pas encore câblée dans le shell, mais l’onglet est sélectionnable et prêt pour la sortie des commandes.", + "Ask the assistant about the active project or editor.": "Interrogez l’assistant sur le projet ou l’éditeur actif.", + "Start chat": "Démarrer la conversation", + "You": "Vous", + "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "La surface de discussion de la barre latérale de l’assistant est prête, mais l’exécution du modèle n’est pas encore connectée." } \ No newline at end of file diff --git a/priv/i18n/locales/it.json b/priv/i18n/locales/it.json index 99e9131..352b865 100644 --- a/priv/i18n/locales/it.json +++ b/priv/i18n/locales/it.json @@ -221,5 +221,9 @@ "View": "Vista", "Welcome to bDS2": "Benvenuto in bDS2", "Workbench Notes": "Note del banco di lavoro", - "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "L’integrazione del working tree non è ancora collegata nella shell, ma la scheda è selezionabile e pronta per l’output dei comandi." + "Working tree integration is not wired yet in the shell, but the tab is selectable and ready for command output.": "L’integrazione del working tree non è ancora collegata nella shell, ma la scheda è selezionabile e pronta per l’output dei comandi.", + "Ask the assistant about the active project or editor.": "Chiedi all’assistente del progetto o editor attivo.", + "Start chat": "Avvia chat", + "You": "Tu", + "The assistant sidebar chat surface is ready, but model execution is not connected yet.": "La superficie chat della barra laterale assistente è pronta, ma l’esecuzione del modello non è ancora collegata." } \ No newline at end of file diff --git a/priv/ui/app.css b/priv/ui/app.css index 9c90673..c567b6c 100644 --- a/priv/ui/app.css +++ b/priv/ui/app.css @@ -2010,6 +2010,134 @@ button { padding: 14px; } +.assistant-sidebar-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.assistant-sidebar-heading { + display: flex; + flex-direction: column; + gap: 4px; +} + +.assistant-sidebar-description, +.assistant-sidebar-context-text, +.assistant-sidebar-message-content { + color: var(--vscode-descriptionForeground); +} + +.assistant-sidebar-status { + border-radius: 999px; + border: 1px solid var(--vscode-panel-border); + padding: 2px 8px; + font-size: 11px; + line-height: 1.4; +} + +.assistant-sidebar-status.is-offline { + background: rgba(255, 196, 0, 0.18); + border-color: rgba(255, 196, 0, 0.35); + color: var(--vscode-editor-foreground); +} + +.assistant-sidebar-context { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; + border: 1px solid var(--vscode-panel-border); + border-radius: 10px; + background: color-mix(in srgb, var(--vscode-sideBar-background) 78%, var(--vscode-editor-background)); +} + +.assistant-sidebar-context-row { + display: flex; + justify-content: space-between; + gap: 12px; +} + +.assistant-sidebar-context-label, +.assistant-sidebar-message-role { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--vscode-descriptionForeground); +} + +.assistant-sidebar-context-value { + text-align: right; + color: var(--vscode-editor-foreground); +} + +.assistant-sidebar-context-text, +.assistant-sidebar-message-content { + margin: 0; + white-space: pre-wrap; +} + +.assistant-sidebar-prompt-form, +.assistant-sidebar-welcome, +.assistant-sidebar-transcript { + display: flex; + flex-direction: column; + gap: 10px; +} + +.assistant-sidebar-prompt { + width: 100%; + min-height: 112px; + resize: vertical; + border: 1px solid var(--vscode-input-border); + border-radius: 10px; + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + padding: 10px 12px; + font: inherit; +} + +.assistant-sidebar-prompt:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: 1px; +} + +.assistant-sidebar-start-button { + align-self: flex-start; + border: 1px solid var(--vscode-button-border, transparent); + border-radius: 999px; + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + padding: 7px 14px; + cursor: pointer; +} + +.assistant-sidebar-start-button:disabled { + cursor: default; + opacity: 0.55; +} + +.assistant-card, +.assistant-sidebar-message { + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px; + border: 1px solid var(--vscode-panel-border); + border-radius: 10px; + border-bottom-width: 1px; + background: color-mix(in srgb, var(--vscode-sideBar-background) 82%, var(--vscode-editor-background)); +} + +.assistant-sidebar-message.user { + background: color-mix(in srgb, var(--vscode-list-hoverBackground) 76%, transparent); +} + +.assistant-sidebar-message.assistant { + background: color-mix(in srgb, var(--vscode-sideBar-background) 70%, var(--vscode-editor-background)); +} + .status-bar { height: 22px; display: flex; diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs index 8b3cda6..62de6ab 100644 --- a/test/bds/desktop/shell_live_test.exs +++ b/test/bds/desktop/shell_live_test.exs @@ -177,6 +177,32 @@ defmodule BDS.Desktop.ShellLiveTest do assert html =~ ">7<" end + test "assistant sidebar exposes context, prompt, and offline-gated transcript" do + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + html = + view + |> element("[data-testid='toggle-assistant']") + |> render_click() + + assert html =~ ~s(data-testid="assistant-shell") + assert html =~ ~s(data-testid="assistant-context") + assert html =~ ~s(data-testid="assistant-prompt-form") + assert html =~ ~s(data-testid="assistant-prompt-input") + assert html =~ ~s(data-testid="assistant-start-button") + assert html =~ ~s(>Dashboard<) + + html = + render_submit(view, "submit_assistant_prompt", %{ + "assistant" => %{"prompt" => "Summarize the current project"} + }) + + assert html =~ ~s(data-testid="assistant-message-user") + assert html =~ ~s(data-testid="assistant-message-assistant") + assert html =~ "Summarize the current project" + assert html =~ "Automatic AI actions stay gated by airplane mode." + end + test "sidebar open supports preview and pin intents for entity tabs" do {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) diff --git a/test/bds/ui/shell_test.exs b/test/bds/ui/shell_test.exs index adbf191..c296acb 100644 --- a/test/bds/ui/shell_test.exs +++ b/test/bds/ui/shell_test.exs @@ -160,4 +160,19 @@ defmodule BDS.UI.ShellTest do assert template =~ "tab-actions" assert template =~ "tab-dirty-indicator" end + + test "desktop shell assets keep the assistant sidebar chat surface contract" do + css = File.read!("/Users/gb/Projects/bDS2/priv/ui/app.css") + template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") + + assert css =~ ".assistant-sidebar-context" + assert css =~ ".assistant-sidebar-prompt" + assert css =~ ".assistant-sidebar-start-button" + assert css =~ ".assistant-sidebar-message" + assert template =~ "data-testid=\"assistant-context\"" + assert template =~ "data-testid=\"assistant-prompt-form\"" + assert template =~ "data-testid=\"assistant-prompt-input\"" + assert template =~ "data-testid=\"assistant-start-button\"" + assert template =~ "assistant-sidebar-transcript" + end end