Description
Implement the AgentChatFeature TCA reducer that consumes AgentStreamEvents from an AgentChatSession and maintains the dialogue transcript state.
Spec: Epic #250 §4.1, §4.3 (data flow); docs/architecture/dialogue-events.md §3 (Mapping on UI).
Scope
File
MacApp/Packages/AgentChat/Sources/AgentChat/Reducer/AgentChatFeature.swift
State
```swift
@ObservableState
public struct State: Equatable {
public var sessionID: SessionID
public var agentKind: AgentKind
public var status: AgentChatStatus
public var transcript: IdentifiedArrayOf
public var toolCalls: IdentifiedArrayOf
public var pendingApproval: ApprovalRequest?
public var inputDraft: String
public var sessionInit: SessionInitInfo?
public var sessionResult: SessionResult?
public var rateLimit: RateLimitInfo?
public var apiRetry: RetryInfo?
public var compactBoundaries: [CompactBoundary] // dividers in transcript
public var scrollAnchorAtBottom: Bool // for auto-scroll logic
public var costAlertShown: Bool
}
```
Actions
```swift
public enum Action {
case onAppear
case inputChanged(String)
case sendMessage
case interruptTurn
case scrollReachedBottom(Bool)
case dismissApproval
case approvalDecision(ApprovalRequest, ApprovalDecision)
case openStreamInspector
// Internal
case _streamEvent(AgentStreamEvent)
case _sessionStatusChanged(AgentChatStatus)
case _startSession(any AgentChatSession)
}
```
Reducer logic
onAppear → start session via Effect, subscribe to its transcript stream.
_streamEvent → dispatch by case to state mutations:
sessionInit(info) → state.sessionInit = info, status = .idle.
messageStarted(id, role, model) → append new AgentMessage with empty content.
textDelta(messageID, text) → locate message, append to last .text segment (create if absent). Throttle 30fps via Effect.debounce (handled at view layer — reducer is immediate).
thinkingStarted(messageID, index) → append .thinking segment, collapsed.
thinkingDelta(messageID, index, text) → append to thinking buffer (no re-render signal — view reads on expand).
toolCallStarted(id, name, kind, messageID) → insert into toolCalls, append .toolCall(id) marker to parent message content.
toolCallInputReady(id, input) → update existing ToolCall.input.
toolCallCompleted(id, output) → status = .completed.
toolCallFailed(id, error) → status = .failed.
apiRetry(...) → state.apiRetry = .init(...); set status = .error transient if attempt ≥ 3.
rateLimitStatus(info) → state.rateLimit = info. If rejected → status = .error.
compactBoundary(...) → append to compactBoundaries.
sessionEnd(result) → state.sessionResult = result, status = .idle (or .error if is_error).
messageCompleted(id, stopReason, usage) → finalize message, update AgentMessage.usage / stopReason.
unknownEvent(raw) → log to Stream Inspector buffer, increment metric.
sendMessage → dispatch session.send(.text(state.inputDraft)), clear draft.
interruptTurn → dispatch session.interrupt().
- Dedup: handled at parser level (D-8); reducer trusts incoming events are unique.
Dependency injection
Via @Dependency(\\.dialogueSessionFactory) — a factory (SessionID, TerminalSession, AgentKind) -> any AgentChatSession. Test injection via test dependency.
Acceptance Criteria
Relationships
Description
Implement the
AgentChatFeatureTCA reducer that consumesAgentStreamEvents from anAgentChatSessionand maintains the dialogue transcript state.Spec: Epic #250 §4.1, §4.3 (data flow); docs/architecture/dialogue-events.md §3 (Mapping on UI).
Scope
File
MacApp/Packages/AgentChat/Sources/AgentChat/Reducer/AgentChatFeature.swiftState
```swift
@ObservableState
public struct State: Equatable {
public var sessionID: SessionID
public var agentKind: AgentKind
public var status: AgentChatStatus
public var transcript: IdentifiedArrayOf
public var toolCalls: IdentifiedArrayOf
public var pendingApproval: ApprovalRequest?
public var inputDraft: String
public var sessionInit: SessionInitInfo?
public var sessionResult: SessionResult?
public var rateLimit: RateLimitInfo?
public var apiRetry: RetryInfo?
public var compactBoundaries: [CompactBoundary] // dividers in transcript
public var scrollAnchorAtBottom: Bool // for auto-scroll logic
public var costAlertShown: Bool
}
```
Actions
```swift
public enum Action {
case onAppear
case inputChanged(String)
case sendMessage
case interruptTurn
case scrollReachedBottom(Bool)
case dismissApproval
case approvalDecision(ApprovalRequest, ApprovalDecision)
case openStreamInspector
}
```
Reducer logic
onAppear→ start session via Effect, subscribe to itstranscriptstream._streamEvent→ dispatch by case to state mutations:sessionInit(info)→state.sessionInit = info,status = .idle.messageStarted(id, role, model)→ append newAgentMessagewith empty content.textDelta(messageID, text)→ locate message, append to last.textsegment (create if absent). Throttle 30fps via Effect.debounce (handled at view layer — reducer is immediate).thinkingStarted(messageID, index)→ append.thinkingsegment, collapsed.thinkingDelta(messageID, index, text)→ append to thinking buffer (no re-render signal — view reads on expand).toolCallStarted(id, name, kind, messageID)→ insert intotoolCalls, append.toolCall(id)marker to parent message content.toolCallInputReady(id, input)→ update existing ToolCall.input.toolCallCompleted(id, output)→ status =.completed.toolCallFailed(id, error)→ status =.failed.apiRetry(...)→state.apiRetry = .init(...); set status =.errortransient if attempt ≥ 3.rateLimitStatus(info)→state.rateLimit = info. If rejected →status = .error.compactBoundary(...)→ append tocompactBoundaries.sessionEnd(result)→state.sessionResult = result,status = .idle(or.errorifis_error).messageCompleted(id, stopReason, usage)→ finalize message, updateAgentMessage.usage / stopReason.unknownEvent(raw)→ log to Stream Inspector buffer, increment metric.sendMessage→ dispatchsession.send(.text(state.inputDraft)), clear draft.interruptTurn→ dispatchsession.interrupt().Dependency injection
Via
@Dependency(\\.dialogueSessionFactory)— a factory(SessionID, TerminalSession, AgentKind) -> any AgentChatSession. Test injection via test dependency.Acceptance Criteria
AgentChatFeaturereducer compiles with all actions/state defined.AgentStreamEventvariants.TestStore— feed a sequence of events, verify final state (minimum: text stream, tool call lifecycle, api_retry, session_end, unknown event).IdentifiedArrayOf<AgentMessage>O(1) updates for textDelta; no full-array rewrite.costAlertShowntriggers whensessionResult.total_cost_usdexceeds threshold from Settings.Relationships