Description
Implement the two message views for Dialogue — user bubble (right side) and assistant bubble (left side, with streaming caret, fade-in, thinking block, tool call markers).
Spec: Epic #250 §6 (UI concept — message level live feel).
Scope
Files
UserMessageView.swift — plain right-aligned bubble.
AssistantMessageView.swift — left-aligned, contains markdown rendering + tool call markers + thinking block.
LiveCaret.swift — reusable blinking cursor ▊ component.
UserMessageView
```swift
public struct UserMessageView: View {
public let message: AgentMessage
}
```
- Renders
message.content as plain text (user input is not markdown-formatted in MVP).
- Right-aligned with
HStack { Spacer() }.
Card(style: .plain) wrapper from DS.
- Max width 80% of available — otherwise balloons with 1-char messages.
AssistantMessageView
```swift
public struct AssistantMessageView: View {
public let message: AgentMessage
public let toolCalls: IdentifiedArrayOf // to look up by ID
public let isStreaming: Bool
}
```
- Renders
message.content[] segments in order:
.text(String) → DialogueMarkdownView(source: text, streaming: isStreaming, cache: shared).
.thinking(Thinking) → ThinkingIndicatorView (D-17).
.toolCall(ToolCallID) → lookup in toolCalls, render via ToolCallCardView (D-15/D-16).
.citation → inline footnote marker.
- Live caret
▊ at end of last .text segment while isStreaming == true.
- Fade-in animation on message appear (
.transition(.opacity.combined(with: .move(edge: .bottom))), DS.Motion.fast).
- Active-streaming glow pulse (1–2% opacity tint overlay) — guarded by
accessibilityReduceMotion.
LiveCaret
Small ▊ character with .opacity animation (0.3 ↔ 1.0, 0.5s cycle). Disabled under reduceMotion (show static).
Accessibility
.accessibilityLabel on each message: "Claude's message" / "Your message".
.accessibilityValue includes content (markdown stripped to plain text for VoiceOver).
- Live region announce when new assistant message completes (
message_stop).
- Thinking block and tool cards — own accessibility; not announced separately here.
Acceptance Criteria
Relationships
Description
Implement the two message views for Dialogue — user bubble (right side) and assistant bubble (left side, with streaming caret, fade-in, thinking block, tool call markers).
Spec: Epic #250 §6 (UI concept — message level live feel).
Scope
Files
UserMessageView.swift— plain right-aligned bubble.AssistantMessageView.swift— left-aligned, contains markdown rendering + tool call markers + thinking block.LiveCaret.swift— reusable blinking cursor▊component.UserMessageView
```swift
public struct UserMessageView: View {
public let message: AgentMessage
}
```
message.contentas plain text (user input is not markdown-formatted in MVP).HStack { Spacer() }.Card(style: .plain)wrapper from DS.AssistantMessageView
```swift
public struct AssistantMessageView: View {
public let message: AgentMessage
public let toolCalls: IdentifiedArrayOf // to look up by ID
public let isStreaming: Bool
}
```
message.content[]segments in order:.text(String)→DialogueMarkdownView(source: text, streaming: isStreaming, cache: shared)..thinking(Thinking)→ThinkingIndicatorView(D-17)..toolCall(ToolCallID)→ lookup intoolCalls, render viaToolCallCardView(D-15/D-16)..citation→ inline footnote marker.▊at end of last.textsegment whileisStreaming == true..transition(.opacity.combined(with: .move(edge: .bottom))),DS.Motion.fast).accessibilityReduceMotion.LiveCaret
Small
▊character with.opacityanimation (0.3 ↔ 1.0, 0.5s cycle). Disabled under reduceMotion (show static).Accessibility
.accessibilityLabelon each message: "Claude's message" / "Your message"..accessibilityValueincludes content (markdown stripped to plain text for VoiceOver).message_stop).Acceptance Criteria
▊shown during streaming, hidden whenisStreaming = false..textSelection(.enabled)allows copying either message content.Relationships