# ADR-004: 前端狀態管理統一採用 Zustand

> **狀態**: Accepted
> **日期**: 2026-03-19
> **更新日期**: 2026-03-20 (Gate 0 驗證完成)
> **決策者**: CTO + CPO

---

## Gate 0 里程碑驗證 (2026-03-20)

**Tracer Bullet 測試通過！** 以下實作已驗證：

| 元件 | Store | 狀態 |
|------|-------|------|
| Dashboard SSE | `dashboard.store.ts` | ✅ 即時同步 |
| Approval Multi-Sig | `approval.store.ts` | ✅ 狀態機運作正常 |
| HITL 簽核流程 | 整合 API `/approvals/{id}/approve` | ✅ TOCTOU 防護驗證 |

---

## 背景

AWOOOI 的前端 (Agent Hub) 需要處理高度頻繁的狀態更新，包括：
- OpenClaw 的 SSE 思考串流 (`/agent/thinking`)
- 即時狀態燈 (Data Pincer 呼吸動畫)
- 待授權卡片的佇列管理 (`/approvals`)
- Plugin 健康狀態即時更新

我們需要一個輕量、無需過度樣板代碼 (Boilerplate)，且能與 React 18 完美協作的狀態管理庫。

## 決策

**全面採用 Zustand 作為全域狀態管理工具**

```typescript
// stores/agent.store.ts
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'

interface AgentState {
  status: 'idle' | 'thinking' | 'executing' | 'waiting_approval'
  thinkingStream: string[]
  pendingApprovals: Approval[]

  // Actions
  setStatus: (status: AgentState['status']) => void
  appendThinking: (chunk: string) => void
  addApproval: (approval: Approval) => void
}

export const useAgentStore = create<AgentState>()(
  subscribeWithSelector((set) => ({
    status: 'idle',
    thinkingStream: [],
    pendingApprovals: [],

    setStatus: (status) => set({ status }),
    appendThinking: (chunk) => set((s) => ({
      thinkingStream: [...s.thinkingStream, chunk]
    })),
    addApproval: (approval) => set((s) => ({
      pendingApprovals: [...s.pendingApprovals, approval]
    })),
  }))
)
```

### 狀態分層策略

| 層級 | 工具 | 用途 |
|------|------|------|
| **全域 UI 狀態** | Zustand | Agent 狀態、Sidebar 開關、Theme |
| **伺服器資料快取** | TanStack Query | API 回應快取、自動重新驗證 |
| **表單狀態** | React Hook Form | 表單驗證、欄位狀態 |
| **元件局部狀態** | useState | 簡單 UI 切換 |

### 禁止事項

```typescript
// ❌ 禁止：Redux
import { createStore } from 'redux'

// ❌ 禁止：Context API 做複雜狀態管理
const GlobalContext = createContext<ComplexState>(...)

// ❌ 禁止：單一巨大 Store
const useGodStore = create(() => ({
  agent: ...,
  plugins: ...,
  pipelines: ...,  // 太多！
}))

// ✅ 正確：Slice Pattern 分拆
const useAgentStore = create(...)
const usePluginStore = create(...)
const usePipelineStore = create(...)
```

## 理由

### 1. 效能優勢

| 特性 | Redux | Zustand |
|------|-------|---------|
| Bundle Size | ~7KB | ~1KB |
| Boilerplate | 高 | 極低 |
| Re-render 控制 | 需 memo/selector | 內建 selector |
| SSE/WebSocket | 需 middleware | 原生支援 |

### 2. SSE 整合範例

```typescript
// hooks/useAgentThinking.ts
export function useAgentThinking() {
  const appendThinking = useAgentStore((s) => s.appendThinking)

  useEffect(() => {
    const eventSource = new EventSource('/v1/agent/thinking')

    eventSource.onmessage = (event) => {
      appendThinking(event.data)  // 直接更新 Zustand
    }

    return () => eventSource.close()
  }, [appendThinking])
}
```

### 3. TanStack Query 協作

```typescript
// hooks/useApprovals.ts
export function useApprovals() {
  return useQuery({
    queryKey: ['approvals', 'pending'],
    queryFn: () => api.listApprovals({ status: 'pending' }),
    refetchInterval: 5000,  // 每 5 秒輪詢
  })
}
```

## 後果

### 優點

- **極度輕量** 不增加 bundle 負擔
- **高頻更新** 完美處理 SSE/WebSocket 串流
- **簡單 API** 降低學習曲線
- **TypeScript 友善** 完整型別推導

### 缺點

- **生態較小** 相比 Redux 社群資源較少
- **DevTools** 功能不如 Redux DevTools 強大

### 風險

| 風險 | 緩解措施 |
|------|---------|
| Store 肥大化 | 強制執行 Slice Pattern，Code Review 把關 |
| 狀態同步錯誤 | 搭配 TanStack Query 管理伺服器狀態 |

---

## Gate 0 實作細節

### 1. Dashboard SSE Store

```typescript
// stores/dashboard.store.ts
interface DashboardState {
  hosts: HostStatus[]
  connectionStatus: 'connecting' | 'connected' | 'disconnected' | 'error'
  lastUpdate: Date | null

  // SSE 控制
  connect: (apiUrl: string) => void
  disconnect: () => void
}

export const useDashboardStore = create<DashboardState>((set, get) => ({
  hosts: [],
  connectionStatus: 'disconnected',
  lastUpdate: null,

  connect: (apiUrl) => {
    const eventSource = new EventSource(`${apiUrl}/api/v1/dashboard/stream`)

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data)
      set({ hosts: data.hosts, lastUpdate: new Date() })
    }

    eventSource.onerror = () => set({ connectionStatus: 'error' })
    eventSource.onopen = () => set({ connectionStatus: 'connected' })
  },

  disconnect: () => {
    // AbortController cleanup
  }
}))
```

### 2. Approval Multi-Sig 狀態機

```typescript
// stores/approval.store.ts
interface ApprovalState {
  pendingApprovals: Approval[]
  selectedApproval: Approval | null
  signingStatus: 'idle' | 'signing' | 'success' | 'error'

  // Actions
  signApproval: (id: string, userId: string, role: string) => Promise<void>
  refreshApprovals: () => Promise<void>
}

// 狀態機轉換圖
// pending → (簽核) → pending (需更多簽章)
// pending → (簽核) → approved (達到閾值)
// pending → (拒絕) → rejected
// pending → (TOCTOU) → voided (資源狀態改變)
```

### 3. SSE + Zustand 整合模式

**企業級 SSE 最佳實踐:**

| 特性 | 實作 |
|------|------|
| **Buffer** | 累積 5 秒內的更新，批次 setState |
| **AbortController** | 元件 unmount 時正確關閉連線 |
| **Reconnection** | 指數退避重連 (1s → 2s → 4s → max 30s) |
| **Heartbeat** | 每 30 秒 ping，超時則重連 |

```typescript
// 企業級 SSE Hook 範例
function useSSE(url: string) {
  const abortControllerRef = useRef<AbortController>()
  const bufferRef = useRef<HostStatus[]>([])

  useEffect(() => {
    abortControllerRef.current = new AbortController()

    const flushBuffer = setInterval(() => {
      if (bufferRef.current.length > 0) {
        useDashboardStore.setState({ hosts: bufferRef.current })
        bufferRef.current = []
      }
    }, 5000)

    return () => {
      abortControllerRef.current?.abort()
      clearInterval(flushBuffer)
    }
  }, [url])
}
```

---

## 參考

- [Zustand](https://zustand-demo.pmnd.rs/)
- [TanStack Query](https://tanstack.com/query)
- ADR-002: Nothing.tech 設計系統 (動畫需求)
- [approvals-contract.yaml](../api/approvals-contract.yaml) - API 契約定義
