package app import ( "github.com/correx/tui-go/internal/protocol" "github.com/correx/tui-go/internal/ws" ) // DisplayState is the top-level screen the UI renders. type DisplayState int const ( StateIdle DisplayState = iota StateInSession StateApproval ) // InputMode toggles what a submitted line targets. type InputMode int const ( ModeRouter InputMode = iota // chat / steering input ModeFilter // session-list filter ) // EditMode is the vim-style modality: Normal = bare-key commands, Insert = typing. type EditMode int const ( ModeNormal EditMode = iota ModeInsert ) // ChatMode selects how a turn is sent to the router. const ( ChatModeChat = "CHAT" ChatModeSteering = "STEERING" ) // OverlayKind is the active modal (immediate-mode: drawn on top when set). type OverlayKind int const ( OverlayNone OverlayKind = iota OverlayPalette OverlayEventInspector OverlayDiff OverlayToolPalette OverlayModels ) // RouterEntry is one line in a session's conversation transcript. type RouterEntry struct { Role string // user | router | tool | narration | narration_llm Content string Metrics *TurnMetrics } // TurnMetrics carries optional latency + token cost for a ROUTER chat turn. type TurnMetrics struct { LatencyMs int64 TotalTokens int } // EventEntry is a row in the event stream. type EventEntry struct { Time string Type string Detail string } // ToolStatus mirrors the Kotlin ToolDisplayStatus. type ToolStatus int const ( ToolStarted ToolStatus = iota ToolCompleted ToolFailed ToolRejected ) type ToolRecord struct { Name string Tier int Status ToolStatus } // ManifestTool is a declared (not-yet-run) tool from a stage manifest. type ManifestTool struct { Name string Tier int } // Approval is a pending approval gate for a session. type Approval struct { RequestID string SessionID string Tier string Risk string ToolName string Preview string // Rationale holds the plane-2 verified preconditions ("[PATH_OUTSIDE_WORKSPACE] …") // that justify the gate — shown in the approval band instead of an opaque tier. Rationale []string } // Session is the UI's view of one server session. type Session struct { ID string Status string WorkflowID string Name string LastEventAt int64 CurrentStage string WorkspaceRoot string // bound cwd, from session.workspace_bound LastOutput string LastResponse string Tools []ToolRecord ToolsByStage map[string][]ManifestTool Events []EventEntry Pending *Approval Active bool // an inference/tool is in flight (drives the spinner) } // Workflow is a launchable workflow advertised by the server. type Workflow struct { ID string Description string } // Model is the whole TUI state. View is a pure function of it (immediate-mode), // which is what keeps overlays from desyncing. type Model struct { width, height int client *ws.Client theme Theme quitting bool // connection connected bool reconnecting bool // sessions sessions []Session selectedID string filter string workflows []Workflow wfIndex int // -1 = not in workflow picker wfVisible bool bgUpdates int // input editMode EditMode inputMode InputMode inputBuffer string inputCursor int history map[string][]string historyIndex int savedBuffer string // flow flags sessionEntered bool approvalDismissed bool pendingWorkflowFocus bool // router transcript routerMessages map[string][]RouterEntry routerConnected bool chatMode string // provider currentModel string providerType string // LOCAL | REMOTE // managed-model swap (nil resource fields = unavailable) availableModels []string modelsIndex int gpuUsedMB *int64 gpuTotalMB *int64 gpuUtil *int ramMB *int64 // diff / overlay overlay OverlayKind overlayEventIdx int diffScrollOffset int eventStripShown bool // command palette paletteFilter string paletteIndex int // animation frame int // tick counter; drives spinner + caret blink // snapshot phase snapshotPhase bool pendingEvents []protocol.ServerMessage // approval steering input buffer steerBuffer string steering bool } // NewModel builds the initial idle state. func NewModel(client *ws.Client) Model { return Model{ client: client, theme: NewTheme(SoftBlue), wfIndex: -1, inputMode: ModeRouter, history: map[string][]string{}, historyIndex: -1, routerMessages: map[string][]RouterEntry{}, chatMode: ChatModeChat, providerType: "LOCAL", snapshotPhase: true, eventStripShown: true, } } // displayState derives the active screen. A session must be *entered* // (sessionEntered) before its in-session or approval surfaces show — otherwise // merely moving the list cursor onto a session with a pending gate would yank // you into the approval, and `l` back-to-list couldn't escape it. func (m Model) displayState() DisplayState { if m.selectedID == "" || !m.sessionEntered { return StateIdle } if m.approvalDismissed { return StateInSession } if s := m.session(m.selectedID); s != nil && s.Pending != nil { return StateApproval } return StateInSession } func (m Model) session(id string) *Session { for i := range m.sessions { if m.sessions[i].ID == id { return &m.sessions[i] } } return nil } // ensureSession returns the session with id, creating an ACTIVE entry if absent. // Session existence is derived from the event stream (any event for an unknown // session vivifies it) rather than a dedicated control frame. func (m *Model) ensureSession(id string) *Session { if s := m.session(id); s != nil { return s } m.sessions = append(m.sessions, Session{ ID: id, Status: "ACTIVE", LastEventAt: nowMillis(), }) return &m.sessions[len(m.sessions)-1] } // filteredSessions applies the workflow-id filter. func (m Model) filteredSessions() []Session { if m.filter == "" { return m.sessions } out := make([]Session, 0, len(m.sessions)) for _, s := range m.sessions { if containsFold(s.WorkflowID, m.filter) { out = append(out, s) } } return out }