Move shared destinations into the one sidebar, and give admin the same shell

d491c17f7214 · AtlantisPleb · · parent e6a56834afb3

Move shared destinations into the one sidebar, and give admin the same shell

Computers, Memory and Leaderboard were rows chat rendered for itself, so they
existed on exactly one page. They are destinations, not conversation
controls, and now sit in the application sidebar for everyone. Memory is the
awkward one: it is the conversation's panel, but reaching it should not
require knowing that, so its state became addressable -- `/chat?panel=memory`
-- and the row is an ordinary link. Arriving by URL loads what arriving by
click loaded, and leaving restores the transcript either way; a panel that
opens empty depending on how you reached it is worse than no panel.

Export went the other direction. It is the conversation's action, so it
belongs beside the conversation's name rather than as a permanent rail row
competing with places you can go. `app/1` gained a `title_menu` slot for
exactly that, and the item now says what it produces: Export JSON (ATIF).

Admin is one footer row for an operator on every page instead of a nav row on
chat. The way out of the memory panel moved into the panel, where it belongs
-- it was previously a sidebar row, which meant leaving depended on chrome
outside the thing you were leaving.

Nine LiveViews called `Layouts.app` without `current_scope`, which is why the
admin surfaces rendered with no sidebar at all and looked like a different
application. Eight now pass it; the UI gallery's live_session does not assign
a scope and keeps its bare frame.

Both homepage buttons were filled: `variant` defaults to `:primary`, so the
secondary action never opted out. Two filled buttons side by side state that
both are the thing to do, which leaves a reader picking rather than
proceeding. The sign-in says "Log in with GitHub" and carries the mark.

`config/dev.exs` now reads the token encryption key from the environment
alongside the OAuth pair. A real local GitHub app issues real tokens, and
encrypting them under a key checked into the repository would make the vault
decorative.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149rBWy7br1Z7bbz9NrQhEr
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified config/dev.exs
  • modified lib/openagents_web/component_catalog.ex
  • modified lib/openagents_web/components/layouts.ex
  • modified lib/openagents_web/live/admin_forge_live.ex
  • modified lib/openagents_web/live/admin_live.ex
  • modified lib/openagents_web/live/admin_scv_accounts_live.ex
  • modified lib/openagents_web/live/changelog_live.ex
  • modified lib/openagents_web/live/chat_live.ex
  • modified lib/openagents_web/live/code_commit_live.ex
  • modified lib/openagents_web/live/code_repo_live.ex
  • modified lib/openagents_web/live/components_live.ex
  • modified lib/openagents_web/live/home_live.ex
  • modified lib/openagents_web/live/network_status_live.ex
  • modified lib/openagents_web/live/voice_spike_live.ex
  • modified test/openagents_web/home_controller_test.exs
  • modified test/openagents_web/icon_affordances_test.exs
  • modified test/openagents_web/live/chat_live_test.exs

Diff

17 files changed, +198 -139

config/dev.exs modified +5 -1

@@ -18,9 +18,13 @@ config :openagents, :github_oauth,

18 18
  client_secret: System.get_env("GITHUB_CLIENT_SECRET") || "dev-client-secret",
19 19
  redirect_uri: "http://localhost:4000/auth/github/callback"
20 20
21
# Overridable like the OAuth pair above: a real local GitHub app issues real
22
# tokens, and encrypting them under a key checked into the repository would
23
# make the vault decorative.
21 24
config :openagents,
22 25
       :github_token_encryption_key,
23
       Base.encode64("openagents-dev-token-vault-key32")
26
       System.get_env("GITHUB_TOKEN_ENCRYPTION_KEY") ||
27
         Base.encode64("openagents-dev-token-vault-key32")
24 28
25 29
config :openagents, :github_token_encryption_key_id, "development-2026-08"
26 30
config :openagents, :github_token_decryption_keys, %{}
lib/openagents_web/component_catalog.ex modified +7

@@ -263,6 +263,13 @@ defmodule OpenAgentsWeb.ComponentCatalog do

263 263
    %{
264 264
      title: "Landing",
265 265
      items: [
266
        %{
267
          slug: "sidebar-footer",
268
          title: "Sidebar footer",
269
          icon: "stack",
270
          source: "OpenAgentsWeb.Layouts.sidebar_footer/1",
271
          summary: "Secondary destinations, gated by environment and role."
272
        },
266 273
        %{
267 274
          slug: "landing-section",
268 275
          title: "Section",
lib/openagents_web/components/layouts.ex modified +53 -4

@@ -47,6 +47,13 @@ defmodule OpenAgentsWeb.Layouts do

47 47
48 48
  slot :inner_block, required: true
49 49
50
  slot :title_menu,
51
    doc: """
52
    Actions belonging to this page, rendered beside its name in the command
53
    bar. Nested, never passed as an attribute: forwarding a slot as an attr
54
    compiles and renders but loses change tracking.
55
    """
56
50 57
  slot :sidebar_extra,
51 58
    doc: """
52 59
    Rows this page contributes to the application sidebar.

@@ -65,7 +72,9 @@ defmodule OpenAgentsWeb.Layouts do

65 72
      </.sidebar>
66 73
67 74
      <div class="flex-1 min-w-0 flex flex-col h-screen">
68
        <.openagents_command_bar current_scope={@current_scope} title={@title} subtitle={@subtitle} />
75
        <.openagents_command_bar current_scope={@current_scope} title={@title} subtitle={@subtitle}>
76
          <:menu>{render_slot(@title_menu)}</:menu>
77
        </.openagents_command_bar>
69 78
70 79
        <main class={[
71 80
          "flex-1 min-w-0",

@@ -95,6 +104,7 @@ defmodule OpenAgentsWeb.Layouts do

95 104
  attr :current_scope, :map, default: nil
96 105
  attr :title, :string, default: nil
97 106
  attr :subtitle, :string, default: nil
107
  slot :menu, doc: "actions belonging to the current page"
98 108
99 109
  defp openagents_command_bar(assigns) do
100 110
    ~H"""

@@ -112,6 +122,7 @@ defmodule OpenAgentsWeb.Layouts do

112 122
              <p class="text-xs text-muted-foreground truncate">{@subtitle}</p>
113 123
            <% end %>
114 124
          </div>
125
          {render_slot(@menu)}
115 126
        <% end %>
116 127
      </div>
117 128

@@ -121,7 +132,7 @@ defmodule OpenAgentsWeb.Layouts do

121 132
          <.account_dropdown current_scope={@current_scope} />
122 133
        <% else %>
123 134
          <.button navigate={~p"/#github-tools"} variant={:primary} size={:sm}>
124
            Sign in with GitHub
135
            <UI.icon name="brand-github" /> Log in with GitHub
125 136
          </.button>
126 137
        <% end %>
127 138
      </div>

@@ -189,12 +200,25 @@ defmodule OpenAgentsWeb.Layouts do

189 200
  The component library is advertised outside production only. It documents
190 201
  the parts a page is built from rather than anything a visitor came for.
191 202
  """
203
  attr :current_user, :map, default: nil, doc: "used only to decide whether admin shows"
204
192 205
  def sidebar_footer(assigns) do
193 206
    assigns =
194
      assign(assigns, :components_link?, OpenAgents.RuntimeConfig.internal_surfaces_visible?())
207
      assigns
208
      |> assign(:components_link?, OpenAgents.RuntimeConfig.internal_surfaces_visible?())
209
      |> assign(:admin_link?, admin?(assigns[:current_user]))
195 210
196 211
    ~H"""
197 212
    <footer class="sidebar-footer">
213
      <.link
214
        :if={@admin_link?}
215
        id="open-admin"
216
        navigate={~p"/admin"}
217
        class="sidebar-footer__link"
218
        aria-label="Admin"
219
      >
220
        <UI.icon name="shield-lock" /> Admin
221
      </.link>
198 222
      <.link :if={@components_link?} navigate={~p"/components"} class="sidebar-footer__link">
199 223
        <UI.icon name="widget" /> Components
200 224
      </.link>

@@ -524,17 +548,42 @@ defmodule OpenAgentsWeb.Layouts do

524 548
          icon="folder"
525 549
          patchable={false}
526 550
        />
551
        <Layouts.sidebar_link
552
          path={~p"/computers"}
553
          label="Computers"
554
          icon="desktop"
555
          patchable={false}
556
        />
557
        <%!-- Memory is the conversation's, but reaching it should not require
558
        knowing that: the row goes to chat with the panel open, which is where
559
        it lives. --%>
560
        <Layouts.sidebar_link
561
          path={~p"/chat?panel=memory"}
562
          label="Memory"
563
          icon="brain"
564
          patchable={false}
565
        />
566
        <Layouts.sidebar_link
567
          path={~p"/leaderboard"}
568
          label="Leaderboard"
569
          icon="trophy-top"
570
          patchable={false}
571
        />
527 572
      </nav>
528 573
529 574
      <%!-- Rows the current page contributes. Chat's destinations, work
530 575
      projections and admin actions arrive here instead of in a second rail. --%>
531 576
      {render_slot(@extra)}
532 577
533
      <Layouts.sidebar_footer />
578
      <Layouts.sidebar_footer current_user={@current_scope} />
534 579
    </aside>
535 580
    """
536 581
  end
537 582
583
  # A nil scope is not an operator. The footer renders on public pages too.
584
  defp admin?(nil), do: false
585
  defp admin?(user), do: OpenAgents.Accounts.admin?(user)
586
538 587
  defp account_display_name(%{github_name: name}) when is_binary(name) and name != "", do: name
539 588
  defp account_display_name(%{github_login: login}), do: "@" <> login
540 589
lib/openagents_web/live/admin_forge_live.ex modified +1 -1

@@ -109,7 +109,7 @@ defmodule OpenAgentsWeb.AdminForgeLive do

109 109
  @impl true
110 110
  def render(assigns) do
111 111
    ~H"""
112
    <Layouts.app flash={@flash}>
112
    <Layouts.app flash={@flash} current_scope={@current_scope}>
113 113
      <main id="admin-forge-page" class="app-shell admin-shell">
114 114
        <section class="admin" aria-label="Forge deploy lane">
115 115
          <h1>Forge — {@repo}</h1>
lib/openagents_web/live/admin_live.ex modified +1 -1

@@ -68,7 +68,7 @@ defmodule OpenAgentsWeb.AdminLive do

68 68
  @impl true
69 69
  def render(assigns) do
70 70
    ~H"""
71
    <Layouts.app flash={@flash}>
71
    <Layouts.app flash={@flash} current_scope={@current_scope}>
72 72
      <main id="admin-page" class="app-shell admin-shell">
73 73
        <%!-- The same bar every other surface renders, so moving between them
74 74
              reads as one application. The lockup carries only the way back:
lib/openagents_web/live/admin_scv_accounts_live.ex modified +1 -1

@@ -104,7 +104,7 @@ defmodule OpenAgentsWeb.AdminScvAccountsLive do

104 104
  @impl true
105 105
  def render(assigns) do
106 106
    ~H"""
107
    <Layouts.app flash={@flash}>
107
    <Layouts.app flash={@flash} current_scope={@current_scope}>
108 108
      <main id="admin-scv-accounts-page" class="app-shell admin-shell">
109 109
        <Layouts.command_bar aria_label="SCV Codex account settings" current_user={@current_user}>
110 110
          <:lockup>
lib/openagents_web/live/changelog_live.ex modified +1 -1

@@ -90,7 +90,7 @@ defmodule OpenAgentsWeb.ChangelogLive do

90 90
  @impl true
91 91
  def render(assigns) do
92 92
    ~H"""
93
    <Layouts.app flash={@flash}>
93
    <Layouts.app flash={@flash} current_scope={@current_scope}>
94 94
      <main id="changelog-page" class="app-shell changelog-shell">
95 95
        <Layouts.command_bar aria_label="OpenAgents changelog" current_user={@current_user}>
96 96
          <:lockup>
lib/openagents_web/live/chat_live.ex modified +72 -95

@@ -2,7 +2,6 @@ defmodule OpenAgentsWeb.ChatLive do

2 2
  use OpenAgentsWeb, :live_view
3 3
4 4
  alias OpenAgents.{
5
    Accounts,
6 5
    Conversations,
7 6
    DataRights,
8 7
    ProfileMemory,

@@ -17,6 +16,7 @@ defmodule OpenAgentsWeb.ChatLive do

17 16
  alias OpenAgents.Voice.Recordings
18 17
  alias OpenAgents.Work
19 18
  alias OpenAgentsWeb.ToolActivity
19
  alias OpenAgentsWeb.UI
20 20
21 21
  # The sidebar's calls and work sections are bounded projections, not
22 22
  # unbounded lists: the last eight of each, recomputed on the same PubSub

@@ -79,6 +79,38 @@ defmodule OpenAgentsWeb.ChatLive do

79 79
    {:ok, socket}
80 80
  end
81 81
82
  @impl true
83
  def handle_params(params, _uri, socket) do
84
    # The sidebar's Memory row is a link, not a chat-local event: it must work
85
    # from any page, so the panel's state is addressable rather than private.
86
    # Arriving by URL has to load what arriving by click loads -- a panel that
87
    # opens empty depending on how you reached it is worse than no panel.
88
    {:noreply, open_memory(socket, params["panel"] == "memory")}
89
  end
90
91
  defp open_memory(socket, true) do
92
    socket
93
    |> assign(:memory_open?, true)
94
    |> assign(:pending_memory_action, nil)
95
    |> assign(:memory_status, nil)
96
    |> reload_memory()
97
  end
98
99
  defp open_memory(socket, false) do
100
    # Closing restores the conversation, which means reloading it: the panel
101
    # replaces the transcript rather than covering it, so coming back has to
102
    # put the messages back whether you left by URL or by control.
103
    {messages, has_older?} = Conversations.list_messages(socket.assigns.conversation)
104
105
    socket
106
    |> assign(:memory_open?, false)
107
    |> assign(:pending_memory_action, nil)
108
    |> assign(:memory_status, nil)
109
    |> assign(:has_older?, has_older?)
110
    |> assign(:oldest_message_id, first_id(messages))
111
    |> stream(:messages, messages, reset: true)
112
  end
113
82 114
  @impl true
83 115
  def handle_event("send_message", %{"chat" => %{"message" => content}}, socket) do
84 116
    case voice_route(socket) do

@@ -137,24 +169,9 @@ defmodule OpenAgentsWeb.ChatLive do

137 169
138 170
  def handle_event("toggle_memory", _params, socket) do
139 171
    if socket.assigns.memory_open? do
140
      {messages, has_older?} = Conversations.list_messages(socket.assigns.conversation)
141
142
      {:noreply,
143
       socket
144
       |> assign(:memory_open?, false)
145
       |> assign(:pending_memory_action, nil)
146
       |> assign(:memory_status, nil)
147
       |> assign(:has_older?, has_older?)
148
       |> assign(:oldest_message_id, first_id(messages))
149
       |> stream(:messages, messages, reset: true)
150
       |> push_event("composer:focus", %{})}
172
      {:noreply, socket |> open_memory(false) |> push_event("composer:focus", %{})}
151 173
    else
152
      {:noreply,
153
       socket
154
       |> assign(:memory_open?, true)
155
       |> assign(:pending_memory_action, nil)
156
       |> assign(:memory_status, nil)
157
       |> reload_memory()}
174
      {:noreply, open_memory(socket, true)}
158 175
    end
159 176
  end
160 177

@@ -801,6 +818,29 @@ defmodule OpenAgentsWeb.ChatLive do

801 818
  def render(assigns) do
802 819
    ~H"""
803 820
    <Layouts.app flash={@flash} title="Chat" current_scope={@current_scope} flush>
821
      <%!-- Export is the conversation's action, so it belongs beside the
822
      conversation's name rather than as a permanent sidebar row competing with
823
      the places you can go. --%>
824
      <:title_menu>
825
        <.button
826
          id="chat-actions-trigger"
827
          variant={:ghost}
828
          size={:sm}
829
          class="chat-actions-trigger"
830
          popovertarget="chat-actions-menu"
831
          popovertargetaction="toggle"
832
          aria-label="Conversation actions"
833
        >
834
          <.icon name="chevron-down" />
835
        </.button>
836
837
        <UI.menu id="chat-actions-menu" label="Conversation actions">
838
          <a id="export-atif" href="/data/export/atif" download role="menuitem" class="menu__item">
839
            <.icon name="download" /> Export JSON (ATIF)
840
          </a>
841
        </UI.menu>
842
      </:title_menu>
843
804 844
      <:sidebar_extra>
805 845
        <.chat_sidebar_rows
806 846
          current_user={@current_user}

@@ -1289,68 +1329,6 @@ defmodule OpenAgentsWeb.ChatLive do

1289 1329
  # any future trailing control floats back above it at its own z-index.
1290 1330
  defp chat_sidebar_rows(assigns) do
1291 1331
    ~H"""
1292
    <nav id="sidebar-nav" class="sidebar-nav" aria-label="Chat">
1293
      <div class="sidebar-row">
1294
        <.link
1295
          id="open-computers"
1296
          navigate="/computers"
1297
          class="sidebar-row__hit"
1298
          aria-label="Computers"
1299
        ></.link>
1300
        <span class="sidebar-row__content">
1301
          <span class="sidebar-row__icon"><.icon name="desktop" /></span>
1302
          <span class="sidebar-row__label">Computers</span>
1303
        </span>
1304
      </div>
1305
1306
      <div class="sidebar-row" data-selected={@memory_open?}>
1307
        <button
1308
          id="toggle-memory"
1309
          type="button"
1310
          class="sidebar-row__hit"
1311
          phx-click="toggle_memory"
1312
          aria-expanded={to_string(@memory_open?)}
1313
          aria-controls="memory-manager"
1314
          aria-label={if @memory_open?, do: "Return to conversation", else: "Memory"}
1315
        ></button>
1316
        <span class="sidebar-row__content">
1317
          <span class="sidebar-row__icon">
1318
            <.icon name={if @memory_open?, do: "arrow-left", else: "brain"} />
1319
          </span>
1320
          <span class="sidebar-row__label">
1321
            {if @memory_open?, do: "Return to conversation", else: "Memory"}
1322
          </span>
1323
        </span>
1324
      </div>
1325
1326
      <div class="sidebar-row">
1327
        <.link
1328
          id="open-leaderboard"
1329
          navigate="/leaderboard"
1330
          class="sidebar-row__hit"
1331
          aria-label="Leaderboard"
1332
        ></.link>
1333
        <span class="sidebar-row__content">
1334
          <span class="sidebar-row__icon"><.icon name="trophy-top" /></span>
1335
          <span class="sidebar-row__label">Leaderboard</span>
1336
        </span>
1337
      </div>
1338
1339
      <div class="sidebar-row">
1340
        <.link
1341
          id="export-atif"
1342
          href="/data/export/atif"
1343
          download
1344
          class="sidebar-row__hit"
1345
          aria-label="Export"
1346
        ></.link>
1347
        <span class="sidebar-row__content">
1348
          <span class="sidebar-row__icon"><.icon name="download" /></span>
1349
          <span class="sidebar-row__label">Export</span>
1350
        </span>
1351
      </div>
1352
    </nav>
1353
1354 1332
    <%!-- Calls and work: bounded, durable-backed projections (last eight
1355 1333
            each), refreshed by the same PubSub broadcasts that drive the
1356 1334
            transcript. A row whose evidence is a durable transcript message is

@@ -1371,22 +1349,11 @@ defmodule OpenAgentsWeb.ChatLive do

1371 1349
      </section>
1372 1350
    </div>
1373 1351
1374
    <nav
1375
      :if={Accounts.admin?(@current_user) or @reset_enabled?}
1376
      id="sidebar-admin"
1377
      class="sidebar-nav"
1378
      aria-label="Administration and data"
1379
    >
1380
      <div :if={Accounts.admin?(@current_user)} class="sidebar-row">
1381
        <.link id="open-admin" navigate="/admin" class="sidebar-row__hit" aria-label="Admin"></.link>
1382
        <span class="sidebar-row__content">
1383
          <span class="sidebar-row__icon"><.icon name="shield-lock" /></span>
1384
          <span class="sidebar-row__label">Admin</span>
1385
        </span>
1386
      </div>
1387
1352
    <%!-- Admin moved to the sidebar footer, where it is one row for an
1353
    operator on every page rather than a row that only exists on chat. What
1354
    stays here is the conversation's own data action. --%>
1355
    <nav :if={@reset_enabled?} id="sidebar-admin" class="sidebar-nav" aria-label="Data">
1388 1356
      <.form
1389
        :if={@reset_enabled?}
1390 1357
        for={%{}}
1391 1358
        id="reset-conversation-form"
1392 1359
        action="/data/reset"

@@ -2149,6 +2116,16 @@ defmodule OpenAgentsWeb.ChatLive do

2149 2116
          </p>
2150 2117
        </div>
2151 2118
        <div class="memory-header__actions">
2119
          <%!-- The way out of a panel belongs in the panel. This used to be a
2120
          sidebar row, which meant leaving depended on chrome outside the thing
2121
          you were leaving. --%>
2122
          <.text_button
2123
            id="toggle-memory"
2124
            phx-click="toggle_memory"
2125
            aria-label="Return to conversation"
2126
          >
2127
            <.icon name="arrow-left" /> Return to conversation
2128
          </.text_button>
2152 2129
          <.text_button id="export-all-data" href="/data/export" download>
2153 2130
            <.icon name="download" /> Export ALL DATA
2154 2131
          </.text_button>
lib/openagents_web/live/code_commit_live.ex modified +1 -1

@@ -123,7 +123,7 @@ defmodule OpenAgentsWeb.CodeCommitLive do

123 123
  @impl true
124 124
  def render(assigns) do
125 125
    ~H"""
126
    <Layouts.app flash={@flash}>
126
    <Layouts.app flash={@flash} current_scope={@current_scope}>
127 127
      <main id="code-commit-page" class="app-shell code-shell">
128 128
        <Layouts.command_bar aria_label="Sarah code" current_user={@current_user}>
129 129
          <:lockup>
lib/openagents_web/live/code_repo_live.ex modified +1 -1

@@ -55,7 +55,7 @@ defmodule OpenAgentsWeb.CodeRepoLive do

55 55
  @impl true
56 56
  def render(assigns) do
57 57
    ~H"""
58
    <Layouts.app flash={@flash}>
58
    <Layouts.app flash={@flash} current_scope={@current_scope}>
59 59
      <main id="code-repo-page" class="app-shell code-shell">
60 60
        <Layouts.command_bar aria_label="OpenAgents code" current_user={@current_user}>
61 61
          <:lockup>
lib/openagents_web/live/components_live.ex modified +16

@@ -942,6 +942,22 @@ defmodule OpenAgentsWeb.ComponentsLive do

942 942
    """
943 943
  end
944 944
945
  defp component_demo(%{item: %{slug: "sidebar-footer"}} = assigns) do
946
    ~H"""
947
    <div class="space-y-3">
948
      <p class="text-sm text-base-content/60">
949
        The foot of every sidebar. Two of its rows are conditional: the component
950
        library is advertised outside production only, and Admin appears for an
951
        operator. Shown here with no user, so Admin is absent — which is what a
952
        visitor sees.
953
      </p>
954
      <div class="demo-frame">
955
        <Layouts.sidebar_footer />
956
      </div>
957
    </div>
958
    """
959
  end
960
945 961
  # ── Landing ───────────────────────────────────────────────────────────────
946 962
  #
947 963
  # These demo at reduced scale inside the documentation column. A hero is
lib/openagents_web/live/home_live.ex modified +9 -2

@@ -42,6 +42,7 @@ defmodule OpenAgentsWeb.HomeLive do

42 42
              <.button
43 43
                id="home-cta-browse"
44 44
                navigate={~p"/OpenAgentsInc/openagents.com/issues"}
45
                variant={:secondary}
45 46
                size={:lg}
46 47
              >
47 48
                View issues

@@ -55,10 +56,16 @@ defmodule OpenAgentsWeb.HomeLive do

55 56
                class="m-0"
56 57
              >
57 58
                <.button type="submit" variant={:primary} size={:lg} id="home-cta-signin">
58
                  Sign in and enable GitHub tools
59
                  <.icon name="brand-github" /> Log in with GitHub
59 60
                </.button>
60 61
              </.form>
61
              <.button navigate={~p"/docs"} size={:lg}>Read the docs</.button>
62
              <%!-- Quieter than the action beside it. `variant` defaults to
63
              `:primary`, so two filled buttons sat side by side stating that
64
              both were the thing to do, which leaves a reader picking rather
65
              than proceeding. --%>
66
              <.button navigate={~p"/docs"} variant={:secondary} size={:lg}>
67
                Read the docs
68
              </.button>
62 69
            <% end %>
63 70
          </:actions>
64 71
lib/openagents_web/live/network_status_live.ex modified +1 -1

@@ -274,7 +274,7 @@ defmodule OpenAgentsWeb.NetworkStatusLive do

274 274
  @impl true
275 275
  def render(assigns) do
276 276
    ~H"""
277
    <Layouts.app flash={@flash}>
277
    <Layouts.app flash={@flash} current_scope={@current_scope}>
278 278
      <main id="network-status-page" class="app-shell status-shell">
279 279
        <Layouts.command_bar aria_label="OpenAgents network status" current_user={@current_user}>
280 280
          <:lockup>
lib/openagents_web/live/voice_spike_live.ex modified +1 -1

@@ -11,7 +11,7 @@ defmodule OpenAgentsWeb.VoiceSpikeLive do

11 11
  @impl true
12 12
  def render(assigns) do
13 13
    ~H"""
14
    <Layouts.app flash={@flash}>
14
    <Layouts.app flash={@flash} current_scope={@current_scope}>
15 15
      <main id="voice-spike" class="app-shell">
16 16
        <header class="command-bar">
17 17
          <div class="brand-lockup"><span class="brand-name">SARAH / VOICE SPIKE</span></div>
test/openagents_web/home_controller_test.exs modified +1 -1

@@ -9,7 +9,7 @@ defmodule OpenAgentsWeb.HomeControllerTest do

9 9
    # not Sarah's. These assertions match the current product identity.
10 10
    assert html =~ "The Agent Forge"
11 11
    assert html =~ ~s(action="/auth/github?github_tools=enabled")
12
    assert html =~ "Sign in and enable GitHub tools"
12
    assert html =~ "Log in with GitHub"
13 13
14 14
    # The scope disclosure was removed from the hero at the owner's direction.
15 15
    # GitHub's own consent screen still states the scope before the grant is
test/openagents_web/icon_affordances_test.exs modified +2 -2

@@ -42,7 +42,7 @@ defmodule OpenAgentsWeb.IconAffordancesTest do

42 42
      conn = log_in_github_user(conn, "icon-decorative-user")
43 43
      {:ok, view, _html} = live(conn, ~p"/chat")
44 44
45
      for id <- ~w(load-older toggle-memory) do
45
      for id <- ~w(load-older) do
46 46
        refute has_element?(view, "##{id} svg[aria-label]"),
47 47
               "the glyph in ##{id} announces itself alongside its text label"
48 48
      end

@@ -53,7 +53,7 @@ defmodule OpenAgentsWeb.IconAffordancesTest do

53 53
      conn = log_in_github_user(conn, "icon-memory-user")
54 54
      {:ok, view, _html} = live(conn, ~p"/chat")
55 55
56
      html = view |> element("#toggle-memory") |> render_click()
56
      html = render_patch(view, ~p"/chat?panel=memory")
57 57
58 58
      assert html =~ "Export ALL DATA"
59 59
      assert html =~ "DELETE ALL DATA"
test/openagents_web/live/chat_live_test.exs modified +25 -26

@@ -52,24 +52,15 @@ defmodule OpenAgentsWeb.ChatLiveTest do

52 52
           end)
53 53
  end
54 54
55
  test "the sidebar leads with Computers immediately above Memory", %{conn: conn} do
55
  test "the sidebar carries Computers, Memory and Leaderboard for everyone", %{conn: conn} do
56 56
    conn = log_in_github_user(conn, "computers-nav-browser")
57 57
    {:ok, view, _html} = live(conn, ~p"/chat")
58 58
59
    assert has_element?(view, ~s(#sidebar-nav #open-computers[aria-label="Computers"]))
60
    assert has_element?(view, ~s(#sidebar-nav #open-computers[href="/computers"]))
61
62
    assert has_element?(
63
             view,
64
             "#sidebar #sidebar-nav > .sidebar-row:first-child #open-computers"
65
           )
66
67
    assert has_element?(
68
             view,
69
             "#sidebar-nav .sidebar-row:has(#open-computers) + .sidebar-row #toggle-memory"
70
           )
71
72
    assert has_element?(view, ~s(#sidebar-nav #open-leaderboard[aria-label="Leaderboard"]))
59
    # Computers, Memory and Leaderboard are destinations for everyone, so they
60
    # sit in the application sidebar rather than appearing only on chat.
61
    assert has_element?(view, ~s(#sidebar a.sidebar-row__hit[href="/computers"]))
62
    assert has_element?(view, ~s(#sidebar a.sidebar-row__hit[href="/chat?panel=memory"]))
63
    assert has_element?(view, ~s(#sidebar a.sidebar-row__hit[href="/leaderboard"]))
73 64
  end
74 65
75 66
  test "chat contributes its rows to the one application sidebar", %{conn: conn} do

@@ -88,8 +79,15 @@ defmodule OpenAgentsWeb.ChatLiveTest do

88 79
89 80
    # Every destination chat used to carry in its own rail is still reachable,
90 81
    # with an accessible name on each stretched hit target.
91
    assert has_element?(view, ~s(#sidebar #toggle-memory[aria-label="Memory"]))
92
    assert has_element?(view, "#sidebar a#export-atif[href='/data/export/atif'][download]")
82
    # Export is the conversation's action, so it is in the conversation's
83
    # header menu rather than a permanent sidebar row.
84
    refute has_element?(view, "#sidebar #export-atif")
85
86
    assert has_element?(
87
             view,
88
             "#chat-actions-menu a#export-atif[href='/data/export/atif'][download]"
89
           )
90
93 91
    assert has_element?(view, "#sidebar #sidebar-sections")
94 92
95 93
    # Identity is the command bar's, once, rather than a second account

@@ -105,8 +103,9 @@ defmodule OpenAgentsWeb.ChatLiveTest do

105 103
106 104
    conn = log_in_admin_user(recycle(conn), "admin-chip-visible-browser")
107 105
    {:ok, view, _html} = live(conn, ~p"/chat")
108
    assert has_element?(view, ~s(#open-admin[aria-label="Admin"]))
109
    assert has_element?(view, "#sidebar-admin .sidebar-row__label", "Admin")
106
    # Admin is one row in the sidebar footer for an operator on every page,
107
    # rather than a row that exists only on chat.
108
    assert has_element?(view, ~s(#sidebar .sidebar-footer #open-admin[href="/admin"]))
110 109
  end
111 110
112 111
  test "the reset control renders only where it is enabled", %{conn: conn} do

@@ -200,7 +199,7 @@ defmodule OpenAgentsWeb.ChatLiveTest do

200 199
    assert {:ok, view, _html} = live(conn, ~p"/chat")
201 200
    assert {:ok, _banned} = OpenAgents.Accounts.ban_user(user, "manual_abuse_review")
202 201
203
    view |> element("#toggle-memory") |> render_click()
202
    view |> form("#message-form", chat: %{message: "still here?"}) |> render_submit()
204 203
    assert_redirect(view, ~p"/")
205 204
  end
206 205

@@ -813,7 +812,7 @@ defmodule OpenAgentsWeb.ChatLiveTest do

813 812
    conn = log_in_github_user(conn, token)
814 813
    assert {:ok, view, _html} = live(conn, ~p"/chat")
815 814
816
    html = view |> element("#toggle-memory") |> render_click()
815
    html = render_patch(view, ~p"/chat?panel=memory")
817 816
818 817
    assert html =~ ~s(id="memory-manager")
819 818
    assert html =~ ~s(aria-labelledby="memory-heading")

@@ -831,7 +830,7 @@ defmodule OpenAgentsWeb.ChatLiveTest do

831 830
    assert has_element?(view, "#delete-data-form button#delete-all-data[type='submit']")
832 831
    refute has_element?(view, "#message-form")
833 832
834
    view |> element("#toggle-memory") |> render_click()
833
    render_patch(view, ~p"/chat")
835 834
    assert has_element?(view, "#message-form")
836 835
    assert render(view) =~ "Hello. I&#39;m Sarah—an OpenAgent. What are we working on?"
837 836
  end

@@ -843,8 +842,8 @@ defmodule OpenAgentsWeb.ChatLiveTest do

843 842
    conn = log_in_github_user(conn, token)
844 843
    assert {:ok, first, _html} = live(conn, ~p"/chat")
845 844
    assert {:ok, second, _html} = live(conn, ~p"/chat")
846
    first |> element("#toggle-memory") |> render_click()
847
    second |> element("#toggle-memory") |> render_click()
845
    render_patch(first, ~p"/chat?panel=memory")
846
    render_patch(second, ~p"/chat?panel=memory")
848 847
849 848
    first
850 849
    |> form("#memory-record-#{record.id} form", %{"claim" => "I prefer concise, direct answers"})

@@ -870,7 +869,7 @@ defmodule OpenAgentsWeb.ChatLiveTest do

870 869
    user = github_user(token)
871 870
    conn = log_in_github_user(conn, token)
872 871
    assert {:ok, view, _html} = live(conn, ~p"/chat")
873
    view |> element("#toggle-memory") |> render_click()
872
    render_patch(view, ~p"/chat?panel=memory")
874 873
875 874
    view |> element("#forget-record-#{record.id}") |> render_click()
876 875
    assert has_element?(view, "#memory-confirmation")

@@ -901,7 +900,7 @@ defmodule OpenAgentsWeb.ChatLiveTest do

901 900
    user = github_user(token)
902 901
    conn = log_in_github_user(conn, token)
903 902
    assert {:ok, view, _html} = live(conn, ~p"/chat")
904
    view |> element("#toggle-memory") |> render_click()
903
    render_patch(view, ~p"/chat?panel=memory")
905 904
906 905
    view |> element("#forget-category-#{project.id}") |> render_click()
907 906
    assert render(view) =~ "Forget every active project memory in this account?"

This page updates live while a promote is in flight · changelog