# TUI 設計取捨

TUI 為何以 bubbletea 單一 package 實作，以及何時該重新檢討。

> 引 pardn chiu：*「bubbletea 的設計不是要拆成互相 reference 的獨立模組——拆了會讓 lifecycle 一團亂。我現在沒餘力處理它。」* 此模組刻意保持不切分。

TUI 住在單一 package（`internal/runtime/tui`），**不**拆成子 package。`internal/runtime/tui/` 底下每個檔案都遵循此原則。

## 為何選 bubbletea（而非 tview / tcell）

先前的 TUI 用 `rivo/tview`，被替換的原因：

- **Inline scrollback**：bubbletea 的 `tea.Println` 寫出的行會捲進 input box 上方 terminal 原生 buffer。tview 佔據整個螢幕，無法與 shell scrollback 共存。
- **lipgloss styling primitives**：border、padding、foreground/background 組合乾淨。tview style 為 tag-based，跨 component 重用較難。
- **bubbles 生態系**：`textarea`、`spinner`、`cursor` 是即插即用的 component，與 charm-bracelet 風格其餘部分一致。

代價是 bubbletea 為 [The Elm Architecture](https://guide.elm-lang.org/architecture/) 的 Go port——其 `tea.Model` 介面設計上即為 monolithic。

## 為何單一 package

`tea.Model` 要求 `Update(tea.Msg) (tea.Model, tea.Cmd)` 為 model type 上的 method。Method 必須與 type 住在同一 package。這強制：

- 所有 `Update` 邏輯與 model 同 package
- 拆成子 package 需要在第三個（root）package 建 wrapper，並匯出**每一個** model field 讓子 package 能讀寫 state
- 目前 `popupState`、`commandPickerState`、`viewMode` 等 `unexported` type 必須變成 exported，形成一個 `internal/runtime/tui` 外部永遠不會消費的「API」
- `send()` 與 `program atomic.Pointer[tea.Program]` 要嘛移進子 package（root 透過 setter API 設定），要嘛留在 root 並強迫 handler import root，這會造成第二個 cycle

真正 Go 風格的 TUI 會建 per-domain widget package（各自持有 state struct、render method 與 event handler），bubbletea 僅作為 event loop。該重構為 600-800 LOC 的重寫，切成 4 個 phase。對目前約 11.2k 非測試 LOC（`internal/runtime/tui`）、由單一開發者維護的 TUI，收益不足以正當化成本。

## 何時重新檢視

當**任一**條件成立時，切換為 per-domain widget package：

- TUI 超過約 3k LOC，且 code review 持續卡在「這該歸屬哪裡」
- 多位開發者常態觸碰 TUI 並互相踩 state
- 特定 widget 需針對凍結 state 做獨立 unit test——目前不實例化整個 `Model` 就辦不到

***

> [!NOTE]
> 本文件由 Claude 讀完完整原始碼後自動生成。
