# `PhoenixKitAgent.Web.Dock`
[🔗](https://github.com/BeamLabEU/phoenix_kit_agent/blob/v0.1.0/lib/phoenix_kit_agent/web/dock.ex#L1)

The assistant in the admin header (spec §10): a button before the
notifications bell, the list of the user's chats under it, and the chat
panel on the right. A sticky LiveView, rendered by core's admin layout as
the module's header widget (`PhoenixKitAgent.header_widgets/0`), so it keeps
its state across admin navigation.

## Whose panel it is

The widget's session (`PhoenixKitAgent.header_widget_session/1`) holds only
the user's uuid and locale. Who acts is decided here, in the **connected**
mount, from the session LiveView hands the panel — the cookie session merged
in (spec §6.5): the session reference (`SessionRef.from_session/1`) and
whether the account is borrowed (`MultiSession.impersonated?/1`). The panel
then works only when the session is not impersonated, belongs to the uuid
of the widget's session, and may use the assistant
(`PhoenixKitAgent.Conversations.authorize_session/2`). Otherwise — and in the
static render, before any of it is known — it renders nothing and every
event is ignored: no list, no chat, no call (spec §8, В13).

Every event goes through `PhoenixKitAgent.Conversations`,
`PhoenixKitAgent.Turns`, `PhoenixKitAgent.Attachments` and
`PhoenixKitAgent.UserConfig` with the reference and the flag, so each one is
checked against the session's scope as it is at that moment; a uuid from the
browser is only ever a chat or a file to look up for that user. A chat's
topic is subscribed to only through `Conversations.subscribe/3`, after its
ownership is checked.

## The feed

Opening a chat reads its latest messages (`Conversations.list_messages/3`,
`latest: true`) into a stream; "Earlier messages" brings the page before
(`before:` the first one shown). A new message is announced on the chat's
topic and read with `list_messages/3` (`after:` the last one shown) or
`get_message/4` — the chat is never read again whole; a message of the user
moves the chat to the top of the list with its title. Only the chat shown is
listened to. A tool call is shown folded, by its summary
(`tool_summary`), never with what the tool returned; a model answer through
`PhoenixKitAgent.Web.Markdown` (no outside pictures, no raw HTML, every link
with its domain and `rel="noopener noreferrer nofollow"`, spec §8.2).

A write the model asked for is a confirmation card in the feed
(`PhoenixKitAgent.Confirmations`): every `tool` message waiting for one is
shown with what `Confirmations.cards/4` says of it — core's preview through
core's `operation_preview` component, the arguments the model sent as plain
text (never markdown or a link), and "Run" / "Cancel the change"; "Ask
again" once it ran out; only "Cancel the change" once the user may no
longer use its tool. Opening a chat first gives the cards core no longer
holds their notice (`Confirmations.settle_lapsed/3`: a click lost with its
node, a row that ran out), so none of them waits on the page or above it.
Opening the chat, "Earlier messages" and every new
message find the cards by their `confirmation_uuid`, so a card still waiting
is waiting again after a reload; the notice that settles a card renders it
again. A card that waits (no notice yet) further back than the page of the
chat the panel loaded is found too (`Confirmations.waiting_cards/3`): it is
shown above the feed (under an id of its own), until "Earlier messages"
reaches it and it takes its place in the feed. A click
runs off this process: a task under `PhoenixKitAgent.Confirmations.TaskSupervisor`,
not linked to the panel, which the panel waits for (`start_async/3`), so a
closed tab does not stop a write half-way. A click while the card is busy
does nothing; a refusal is shown on the card, which keeps waiting.

A chat that is locked by a turn lost with its node gets the notice "the
answer was interrupted" when it is opened (`Turns.settle_interrupted/3`).
A notice is shown by its `error` code in the panel's language
(`PhoenixKitAgent.Web.Dock.Texts.notice/1`), else as it was stored.
"New chat" only empties the panel: the chat is made with its first message
or file, and deleted again (`Conversations.delete_conversation/3`) when that
message or file is refused while nothing else is in it.

## Sending, stopping, files

"Send" starts a turn (`Turns.start/6`) with the files that are ready, the
page the browser is on (a path, for the model's context, spec §7.1 p.5) and
the user's locale; an exception there is caught, logged as its class only,
and shown as a notice in the panel. A send while the panel shows a turn
first asks the chat's lock (`Turns.settle_interrupted/3`): while the turn
runs the send is let go; a turn whose end never reached the panel is let go
of, and the message goes. A chat busy with a turn of another tab shows that
turn, with its "Stop".
"Stop" asks the turn to stop — on whichever node runs it (`Turns.stop/4`
passes the request on over PubSub while the chat's lock is within its
time); nothing answers it but the turn's end, so when that has not come
within the grace period and one more check of the lock, the panel says the
answer has not stopped and offers "Stop" again (a turn lost with its node
frees the chat when its lock expires). When `Turns.stop/4` finds no such
turn, the chat's lock says what to show. The indicator says whether the model thinks or a
tool is called; in the browser it gives up after the deadline of a turn and
its grace period (`Monitor.stop_grace_ms/0`), counted again for each turn
and after a lost connection is found again.
The indicator follows the server only: while a turn is shown as running,
the panel reads the chat's lock again every `:turn_check_ms` (application
environment, default 5 s), so a turn whose end never reached it — it came
between the read of the chat and the subscription, or was lost between
nodes — is let go of, with the messages it left.

Files come through `allow_upload` (the kinds and the size of §9, the number
of the user's settings) and are handed to `Attachments.add/4` with the path
of LiveView's own temporary file (`consume_uploaded_entry/3`, kept until
`add/4` is done, which runs off this process); the row is read again after
`add/4` (it may be settled already) and shown only while its chat is the
one open, and a failed one can be removed. An
upload can be cancelled while it is on its way, not once `add/4` has it; a
cancel of an entry that is not there changes nothing. An exception in
`add/4` is caught in its task and logged as its class only.

## In the browser

The panel opens with the class `pk-dock-open` on `<body>`
(`PhoenixKitWeb.Components.AdminDock`), which survives a live redirect. Open
or closed, the chosen chat and the width are kept in `localStorage` by the
`.DockState` hook and restored (`restore`) when the panel mounts again
(another `live_session`, a reload) and when it joins again after a lost
connection, which mounts the LiveView afresh; the turn shown then is the
one the chat's lock holds now, and every restore patches the whole panel,
so nothing the browser kept from before the connection was lost stays. A
panel kept closed reads
nothing until a chat is opened; a remembered chat that is not the user's
active one any more is forgotten. The list opens from a focusable `div`
(Safari does not focus a clicked `<button>`, and the list is open while it
has the focus); choosing a chat moves the focus to the panel, "New chat" to
the input, so the list closes. The markup and the hooks are in
`PhoenixKitAgent.Web.Dock.Panel`; the hooks are runtime colocated hooks, so
the host bundles nothing for them, but they are inline scripts: under a
Content-Security-Policy the page needs `script-src 'unsafe-inline'` (they
carry no nonce). The text being written stays the browser's
(`phx-update="ignore"`), so no re-render wipes it, and goes to the server
only with the form (`phx-debounce="blur"`).

The tools get the user's locale, the endpoint's URL and, when the host's
socket passes `:peer_data` / `:x_headers` in its `connect_info`, the client
address as their request context.

## Nothing in the log

`log: false`: LiveView would log every event with its parameters — the
user's text. No text of a message, an answer or a file name is logged here.
An exception in an event, a message of the chat, an upload's progress or a
finished task is caught (`guarded/2`), logged as its class only, and shown
as a notice: a crash report would hold the event's parameters (the draft)
and the whole state (the chats, the feed). Not covered: a crash in `mount`
or in `render` still ends the LiveView with such a report.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
