#region AgentChat.Ux [C:3] [TYPE ADR] [SEMANTICS ux,agent-chat,panel,native-stack]
@defgroup Ux UX contract — Gradio submit() + LangChain v1 astream_events(). Structured JSON metadata (ChatMessage.metadata). No REST, no WebSocket, no follower.

## FSM

```
idle ──[submit("/chat")]──→ streaming ──[metadata.type: stream_token]──→ (tokens appear)
                               streaming ──[metadata.type: tool_start]──→ (tool card + spinner)
                               streaming ──[metadata.type: tool_end]──→ (tool done, checkmark)
                               streaming ──[metadata.type: tool_error]──→ (tool error, cross)
                               streaming ──[metadata.type: confirm_required]──→ awaiting_confirmation
                                   awaiting_confirmation ──[2nd submit(__resume__,"confirm")]──→ streaming (confirm_resolved)
                                   awaiting_confirmation ──[2nd submit(__resume__,"deny")]──→ idle (confirm_resolved: denied)
                                   awaiting_confirmation ──[timeout 5min]──→ idle (expired)
                               streaming ──[final chunk]──→ idle
                               streaming ──[submission.cancel()]──→ idle (partial)
                               streaming ──[metadata.type: error]──→ error

idle ──[load history]──→ loading_history ──[success]──→ idle
                           loading_history ──[failure]──→ error

idle ──[connect fail]──→ disconnected ──[retry 5×5s]──→ idle
                            disconnected ──[retry exhausted]──→ disconnected_permanent
```

## State Mappings

| @UX_STATE | Visual | ARIA | User Can |
|-----------|--------|------|----------|
| idle (with msgs) | Input enabled, Send active, Stop hidden. Connection dot green. | — | Send, switch/delete convos, expand to /agent |
| idle (new convo) | Welcome card + examples chips (Gradio `examples`). | `aria-label="Новый диалог"` | Click chip, type |
| streaming | Tokens appear in real time. Tool cards inline. Send→Stop (destructive). Input locked. | `aria-live="polite" aria-busy="true"` | Stop, expand tool cards |
| awaiting_confirmation | Streaming pauses. Warning card: `bg-warning-light border-warning-DEFAULT`. Prompt text + Confirm/Deny buttons. Timer 5:00→0:00. | `role="alertdialog"` | Confirm, Deny |
| awaiting_confirmation (expired) | Card stays. Buttons disabled. Timer=0. "Повторить запрос?" | — | Retry |
| error | Card: `bg-destructive-light border-destructive-ring`. Error + Retry. Partial text preserved. | `role="alert"` | Retry, new message |
| disconnected | Dot red. Banner: "Агент недоступен. Переподключение через Xс..." | `role="alert"` | Read history, manual reconnect |
| disconnected_permanent | Dot red. Manual reconnect button only. | `role="alert"` | Manual reconnect |
| loading_history | Skeleton cards (3-5, `animate-pulse bg-surface-muted`). Input locked. | `aria-busy="true"` | Wait |

## Feedback

| Trigger | Feedback |
|---------|----------|
| `metadata.type: stream_token` | Append token to assistant message |
| `metadata.type: tool_start` | Tool card: name (mono) + spinner |
| `metadata.type: tool_end` | Spinner→checkmark, result in tooltip |
| `metadata.type: tool_error` | Spinner→cross, error in tooltip |
| `metadata.type: confirm_required` | Confirmation card + timer, Confirm/Deny |
| `metadata.type: confirm_resolved(result=confirmed)` | Card collapses, streaming resumes |
| `metadata.type: confirm_resolved(result=denied)` | Card collapses, agent responds |
| Stream complete | Stop→Send, input enabled |
| Connection lost | Dot red. Banner. Retry countdown |
| Connection restored | Dot green. Banner dismissed |

## Recovery

| From | Action | To |
|------|--------|-----|
| error | Retry (re-sends last msg) | streaming |
| disconnected | Auto-retry 5×5s or manual | idle |
| awaiting_confirmation (expired) | "Повторить запрос" | streaming |
| streaming (cancelled) | Type new msg | idle→streaming |
| empty (no convos) | Click chip or type | streaming |

## Reactivity

- `AgentChat.Model` atoms → component props → DOM
- `@gradio/client` `submit()` → `for await (event)` → `model._onStreamData()` → `$state` update
- No `$effect` for data loading — all via stream events
- `assistantChatStore.conversationId` → model reads (read-only)

## UX Tests

| @UX_TEST | Given | When | Then |
|----------|-------|------|------|
| Happy path | idle (new) | Type "привет", Send | streaming, tokens appear, idle |
| Tool call | streaming | Agent calls tool | Card with spinner→checkmark |
| Dangerous confirm | streaming | Agent wants deploy | "⏸️" card, confirm_id, timer |
| Confirm accepted | awaiting_confirmation | 2nd submit(__resume__) | "▶️", streaming resumes |
| Confirm denied | awaiting_confirmation | 2nd submit(deny) | "⏹️", idle |
| Stream cancel | streaming | submission.cancel() | idle, partial text |
| LLM error | streaming | LLM fails | "Ошибка: ..." + retry |
| Gradio disconnect | idle | Gradio dies | Dot red, 5×5s retry |
| Multi-tab gate | idle | Tab 2 opens same convo | Tab 2: send rejected |
| Empty convo | idle (new) | Load | Examples chips visible |
| History load error | idle | API fails | Error + retry |

#endregion AgentChat.Ux
