Phase 4: TUI v2 顶层 App 与状态模型
- Phase: 4
- 优先级: P0
- 依赖: Phase 1(独立二进制可启动)、Phase 2(GatewayClient 契约已定义)
What — 要做什么
在 internal/tuiv2/ 中建立 TUI v2 的顶层 App 组件和集中式 ViewModel 状态模型。
App 组件
internal/tuiv2/app.go 中的 App struct 实现 tea.Model,是 TUI v2 的根组件:
type App struct {
client gateway.Client // 数据源(fake 或真实)
state *state.ViewState // 集中式 ViewModel
debug bool
// 子组件(后续阶段逐个接入)
ambientStatus *AmbientStatus
agentStream *AgentStream
commandPrompt *CommandPrompt
softInspector *SoftInspector
// ...
}
职责:
Init() — 调用 client.Health() 检查连接,初始化 ViewState
Update(msg tea.Msg) — 路由消息到子组件,处理全局消息(tea.WindowSizeMsg, tea.KeyMsg)
View() — 自上而下拼接子组件视图
ViewModel 状态模型
internal/tuiv2/state/viewstate.go 定义集中式 ViewModel:
type ViewState struct {
Gateway GatewayState // 连接、会话列表、模型信息
Runtime RuntimeState // 当前 run 状态
Stream []StreamEntry // 消息流条目(不可变)
Input InputState // 输入区状态
Layout LayoutState // 布局尺寸
Mode InputMode // normal | input | leader
}
type GatewayState struct {
Connected bool
Sessions []gateway.SessionSummary
ActiveSess *gateway.SessionSummary
Models []gateway.ModelInfo
ActiveModel string
}
type RuntimeState struct {
Phase string // idle | running | waiting_permission | waiting_user | cancelled | error
RunID string
Tokens TokenUsage
}
type TokenUsage struct {
Input int
Output int
Total int
}
type StreamEntry struct {
ID string
Type string // message | tool_start | tool_end | permission | question | status
Timestamp time.Time
Content string
ToolName string
ToolInput string
Metadata map[string]any
}
type InputState struct {
Text string
Cursor int
Mode string // command | message | permission_response | question_answer
Prompt string // 权限提示或问题文本
Options []string // ask_user 选项
}
type LayoutState struct {
Width int
Height int
InspectorWidth int // Soft Inspector 宽度,0 表示隐藏
ShowInspector bool
}
type InputMode int
const (
InputMode InputMode = iota // 默认:打字、发送
NormalMode // Esc:导航、搜索
LeaderMode // Space:命令
)
关键约束
state 包 禁止 import internal/runtime, internal/session, internal/repository, SQLite
ViewState 字段全部导出,不设 getter/setter
StreamEntry 不可变 —— 新事件 = 新 entry,追加而非修改
- 所有数据来自
gateway.Client,组件不直接持有假数据
Why — 为什么集中式 ViewModel
- 单一数据源:所有组件读取同一份
ViewState,避免子组件各自维护状态副本导致不一致
- 渲染可预测:
ViewState 变化 → View() 重新计算 → 屏幕更新,数据流单向
- 测试友好:可以构造任意
ViewState 快照来测试组件渲染,无需启动完整 TUI
- Gateway 事件 → ViewModel 映射清晰:事件处理逻辑集中在 reducer 中(Phase 7),组件只管渲染
为什么 StreamEntry 不可变:
- Agent 对话流是追加式的时间序列,不应出现"修改历史消息"的语义
- 不可变 entry 使渲染 diff 简单:只需比较
len(stream) 和最后一个 entry 的 ID
How — 怎么做
- 创建
internal/tuiv2/app.go,定义 App struct 和 tea.Model 三方法
- 创建
internal/tuiv2/state/viewstate.go,定义 ViewState 和子结构体
App.Init() 调用 client.Health(),返回初始 ViewState
App.Update() 处理 tea.WindowSizeMsg 更新 LayoutState
App.View() 返回占位文本(逐行输出 ViewState 关键字段),后续 Phase 逐步替换为真实组件
- 更新
cmd/neocode-tuiv2/main.go,将 StartupConfig 传入 tuiv2.NewApp()
验收标准
Phase 4: TUI v2 顶层 App 与状态模型
What — 要做什么
在
internal/tuiv2/中建立 TUI v2 的顶层 App 组件和集中式 ViewModel 状态模型。App 组件
internal/tuiv2/app.go中的Appstruct 实现tea.Model,是 TUI v2 的根组件:职责:
Init()— 调用client.Health()检查连接,初始化ViewStateUpdate(msg tea.Msg)— 路由消息到子组件,处理全局消息(tea.WindowSizeMsg,tea.KeyMsg)View()— 自上而下拼接子组件视图ViewModel 状态模型
internal/tuiv2/state/viewstate.go定义集中式 ViewModel:关键约束
state包 禁止 importinternal/runtime,internal/session,internal/repository, SQLiteViewState字段全部导出,不设 getter/setterStreamEntry不可变 —— 新事件 = 新 entry,追加而非修改gateway.Client,组件不直接持有假数据Why — 为什么集中式 ViewModel
ViewState,避免子组件各自维护状态副本导致不一致ViewState变化 →View()重新计算 → 屏幕更新,数据流单向ViewState快照来测试组件渲染,无需启动完整 TUI为什么
StreamEntry不可变:len(stream)和最后一个 entry 的 IDHow — 怎么做
internal/tuiv2/app.go,定义Appstruct 和tea.Model三方法internal/tuiv2/state/viewstate.go,定义ViewState和子结构体App.Init()调用client.Health(),返回初始ViewStateApp.Update()处理tea.WindowSizeMsg更新LayoutStateApp.View()返回占位文本(逐行输出ViewState关键字段),后续 Phase 逐步替换为真实组件cmd/neocode-tuiv2/main.go,将StartupConfig传入tuiv2.NewApp()验收标准
internal/tuiv2/state/包不 import 任何后端包App实现了tea.ModelApp.Init()调用client.Health()并返回初始状态tea.WindowSizeMsg正确更新LayoutState.Width/Heightgo build ./internal/tuiv2/...成功go test ./internal/tuiv2/...成功