feat: add Go/Bubble Tea TUI rewrite (apps/tui-go)
Reimplement the TUI in Go using Bubble Tea, replacing the Kotlin/Tamboui app. Same WebSocket protocol, soft-rounded layout with blue accent theme.
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user