Description
Implement the top-level DialogueView — the full chat surface shell that composes messages, scroll with anchored auto-follow, top status strip, error banners, and input field.
Spec: Epic #250 §6 (UI concept — session level live feel); docs/architecture/dialogue-events.md §3 (mapping).
Scope
File
MacApp/Packages/AgentChatUI/Sources/AgentChatUI/DialogueView.swift
Structure
```swift
public struct DialogueView: View {
public let store: StoreOf
public init(store: StoreOf)
}
```
Layout (top to bottom):
Compact status strip — thin bar above input:
idle → Model + session cost: "Claude opus-4-7 · $0.12" (greyed).
.streaming → Responding… with animated dots.
.thinking → Thinking… with brain icon.
Tool in progress → Running: {tool_name} with tool icon.
.error or api_retry → Retrying… (2/5 · rate_limit · 4s) with warning icon.
rate_limit.rejected → red "Rate limit exceeded · resets at 14:30" blocking banner.
Scroll area — ScrollViewReader + LazyVStack:
Optional compact_boundary divider lines between messages: ── History compacted ({trigger}) ──.
For each AgentMessage: dispatch to UserMessageView / AssistantMessageView based on role.
Auto-scroll to bottom when new message appears only if user was at bottom (tracked via scrollAnchorAtBottom state).
When user scrolled up + new message arrives — floating chip "↓ N new" overlay at bottom-right; click chip → scroll-to-bottom.
Input section — bottom:
TextEditor bound to state.inputDraft.
Submit button (right): ⌘Return or click.
When streaming: submit replaced by "Stop" / "Interrupt" button (⌘. or click).
When error / rate limit rejected: disabled with reason chip.
Placeholder: "Ask Claude…".
Error banners — stacked above input, dismissible:
plugin_errors → yellow banner with list.
sessionResult.is_error → red banner with subtype and errors.
costAlertShown (> threshold from Settings) → yellow banner with cost and pause/continue action.
Scroll logic
Use scrollPosition(id:) API (macOS 15+ / iOS 18+).
Track scrollAnchorAtBottom: Bool via onScrollTargetVisibilityChange or position delta.
state.scrollAnchorAtBottom updates from Action.scrollReachedBottom(_:) dispatched by view.
Focus management
Input has initial focus on view appear (FocusState).
⌘Return submits; ⌘. interrupts.
Esc clears input draft.
A11y
.accessibilityElement(children: .contain) on scroll area.
Live region announces streaming state changes at the session level (not per-token).
Input has .accessibilityLabel("Message input") + .accessibilityHint("Press Command Return to send").
Acceptance Criteria
DialogueView composes all layers; full flow works end-to-end against a stub session feeding pre-recorded JSONL.
Auto-scroll honors user scroll position (test: scroll up, new message arrives → no auto-scroll, chip appears; click chip → scrolls).
Status strip reflects all state variants (idle, streaming, thinking, tool-in-progress, error, api_retry, rate limit rejected).
Input ⌘Return sends; ⌘. interrupts; Esc clears.
Cost alert banner appears when threshold exceeded, dismissable.
VoiceOver reads messages in order, announces streaming state changes.
All DS tokens.
Snapshot tests for representative states: empty session, streaming, tool running, error, rate-limited.
Relationships
Description
Implement the top-level
DialogueView— the full chat surface shell that composes messages, scroll with anchored auto-follow, top status strip, error banners, and input field.Spec: Epic #250 §6 (UI concept — session level live feel); docs/architecture/dialogue-events.md §3 (mapping).
Scope
File
MacApp/Packages/AgentChatUI/Sources/AgentChatUI/DialogueView.swiftStructure
```swift
public struct DialogueView: View {
public let store: StoreOf
public init(store: StoreOf)
}
```
Layout (top to bottom):
idle→ Model + session cost: "Claude opus-4-7 · $0.12" (greyed)..streaming→Responding…with animated dots..thinking→Thinking…with brain icon.Running: {tool_name}with tool icon..erroror api_retry →Retrying… (2/5 · rate_limit · 4s)with warning icon.rate_limit.rejected→ red "Rate limit exceeded · resets at 14:30" blocking banner.ScrollViewReader+LazyVStack:compact_boundarydivider lines between messages:── History compacted ({trigger}) ──.AgentMessage: dispatch to UserMessageView / AssistantMessageView based on role.scrollAnchorAtBottomstate).TextEditorbound tostate.inputDraft.plugin_errors→ yellow banner with list.sessionResult.is_error→ red banner with subtype and errors.costAlertShown(> threshold from Settings) → yellow banner with cost and pause/continue action.Scroll logic
scrollPosition(id:)API (macOS 15+ / iOS 18+).scrollAnchorAtBottom: BoolviaonScrollTargetVisibilityChangeor position delta.state.scrollAnchorAtBottomupdates fromAction.scrollReachedBottom(_:)dispatched by view.Focus management
FocusState).A11y
.accessibilityElement(children: .contain)on scroll area..accessibilityLabel("Message input")+.accessibilityHint("Press Command Return to send").Acceptance Criteria
DialogueViewcomposes all layers; full flow works end-to-end against a stub session feeding pre-recorded JSONL.Relationships