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 ) // RouterEntry is one line in a session's conversation transcript. type RouterEntry struct { Role string // user | router | tool Content string } // 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 } // 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 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 // router transcript routerMessages map[string][]RouterEntry routerConnected bool chatMode string // provider currentModel string providerType string // LOCAL | REMOTE // 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, matching the Kotlin extension. func (m Model) displayState() DisplayState { if m.selectedID == "" { return StateIdle } hasApproval := false if s := m.session(m.selectedID); s != nil && s.Pending != nil { hasApproval = true } switch { case m.approvalDismissed: return StateInSession case hasApproval: return StateApproval case m.sessionEntered: return StateInSession default: return StateIdle } } func (m Model) session(id string) *Session { for i := range m.sessions { if m.sessions[i].ID == id { return &m.sessions[i] } } return nil } // 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 }