Skip to content

Phase 4: TUI v2 顶层 App 与状态模型 #665

@pionxe

Description

@pionxe

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

  1. 单一数据源:所有组件读取同一份 ViewState,避免子组件各自维护状态副本导致不一致
  2. 渲染可预测ViewState 变化 → View() 重新计算 → 屏幕更新,数据流单向
  3. 测试友好:可以构造任意 ViewState 快照来测试组件渲染,无需启动完整 TUI
  4. Gateway 事件 → ViewModel 映射清晰:事件处理逻辑集中在 reducer 中(Phase 7),组件只管渲染

为什么 StreamEntry 不可变:

  • Agent 对话流是追加式的时间序列,不应出现"修改历史消息"的语义
  • 不可变 entry 使渲染 diff 简单:只需比较 len(stream) 和最后一个 entry 的 ID

How — 怎么做

  1. 创建 internal/tuiv2/app.go,定义 App struct 和 tea.Model 三方法
  2. 创建 internal/tuiv2/state/viewstate.go,定义 ViewState 和子结构体
  3. App.Init() 调用 client.Health(),返回初始 ViewState
  4. App.Update() 处理 tea.WindowSizeMsg 更新 LayoutState
  5. App.View() 返回占位文本(逐行输出 ViewState 关键字段),后续 Phase 逐步替换为真实组件
  6. 更新 cmd/neocode-tuiv2/main.go,将 StartupConfig 传入 tuiv2.NewApp()

验收标准

  • internal/tuiv2/state/ 包不 import 任何后端包
  • App 实现了 tea.Model
  • App.Init() 调用 client.Health() 并返回初始状态
  • tea.WindowSizeMsg 正确更新 LayoutState.Width/Height
  • go build ./internal/tuiv2/... 成功
  • go test ./internal/tuiv2/... 成功

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions