tui

package
v0.48.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 4, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

internal/agent/tui/attach_picker.go

internal/agent/tui/cmds.go

internal/agent/tui/keymap.go

internal/agent/tui/login_panel.go

internal/agent/tui/logout_panel.go

internal/agent/tui/model.go

internal/agent/tui/panels.go

internal/agent/tui/runtime_cwd.go

internal/agent/tui/styles.go

internal/agent/tui/view.go

Index

Constants

This section is empty.

Variables

View Source
var (
	StyleBorder       = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(0, 1)
	StylePanelTitle   = lipgloss.NewStyle().Bold(true)
	StyleStatusBar    = lipgloss.NewStyle().Background(lipgloss.Color("#222")).Foreground(lipgloss.Color("#ccc"))
	StyleStatusBarErr = StyleStatusBar.Foreground(lipgloss.Color("#FF7A7A"))
	StyleHint         = lipgloss.NewStyle().Faint(true)
	StyleAuthErr      = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF7A7A")).Bold(true)
	StyleAuthOk       = lipgloss.NewStyle().Foreground(lipgloss.Color("#5FFF87"))
)

Functions

func RenderView

func RenderView(m *Model) string

RenderView produces the full screen text for the Model. Layout:

[statusBar 2 lines]
[viewport (scrolling timeline)]
[activePanel (optional)]
[input area + LoggedOut hint]

Types

type APIError

type APIError struct {
	HTTPStatus int
	Code       string `json:"code"`
	Message    string `json:"message"`
}

APIError is returned for any 4xx/5xx response with the standard {"error":{"code":"...","message":"..."}} body. Code may be empty for non-standard responses.

func (*APIError) Error

func (e *APIError) Error() string

type AskUserPanelInput

type AskUserPanelInput struct {
	QID         string
	Question    string
	Options     []string
	MultiSelect bool
}

type AttachReplyMsg

type AttachReplyMsg struct {
	Resp *AttachResponse
	Err  error
}

type AttachResponse

type AttachResponse struct {
	SessionID         string  `json:"session_id"`
	PermResponder     *string `json:"permission_responder"`
	PreviousResponder string  `json:"previous_responder"`
	PreviousPreferred string  `json:"previous_preferred"`
}

type AttachmentPickedMsg

type AttachmentPickedMsg struct{ Attachment InboundAttachment }

type AttachmentRemovedMsg

type AttachmentRemovedMsg struct{ Index int }

type AuthConfig

type AuthConfig struct {
	ServerURL       string
	CredentialsPath string
	SkipOpenBrowser bool
	OnChange        func(AuthState)

	// OnLoginFailed is called (from the poll goroutine) when the OAuth Device
	// Flow fails for any reason other than user cancellation (context cancel).
	// nil if caller doesn't need this signal.
	OnLoginFailed func(error)

	// Test seams (default to real implementations from internal/agent/login.go).
	RequestDeviceCode func(serverURL string) (*agent.DeviceAuthResponse, error)
	PollForToken      func(serverURL string, dr *agent.DeviceAuthResponse) (*agent.TokenResponse, error)
}

AuthConfig holds configuration for AuthController.

type AuthController

type AuthController struct {
	// contains filtered or unexported fields
}

AuthController manages the OAuth authentication state machine.

func NewAuthController

func NewAuthController(cfg AuthConfig) *AuthController

NewAuthController creates an AuthController and initialises state from stored credentials. If valid credentials exist at cfg.CredentialsPath the state starts as AuthLoggedIn.

func (*AuthController) CancelLogin

func (a *AuthController) CancelLogin()

CancelLogin aborts an in-progress login flow.

func (*AuthController) EnsureValid

func (a *AuthController) EnsureValid(ctx context.Context) (string, error)

EnsureValid returns a non-empty access token or an error. If the token is near expiry it triggers a refresh (state → Refreshing). Refresh failure transitions to LoggedOut.

func (*AuthController) Logout

func (a *AuthController) Logout() error

Logout clears in-memory credentials and invalidates the credentials file so that subsequent LoadCredentials calls return an error (file contains no parseable content).

func (*AuthController) SetOnChange

func (a *AuthController) SetOnChange(fn func(AuthState))

SetOnChange installs or replaces the OnChange callback after construction. Used by RunTUI to wire the Bubble Tea program after both AuthController and the program exist.

func (*AuthController) SetOnLoginFailed

func (a *AuthController) SetOnLoginFailed(fn func(error))

SetOnLoginFailed installs or replaces the OnLoginFailed callback after construction. Used by RunTUI to surface login errors to the TUI timeline.

func (*AuthController) StartLogin

func (a *AuthController) StartLogin(ctx context.Context) (LoginInfo, error)

StartLogin kicks off OAuth Device Flow. Returns the user-visible code and URL synchronously; the polling loop runs in a goroutine and eventually transitions state to LoggedIn or LoggedOut.

func (*AuthController) State

func (a *AuthController) State() AuthState

State returns the current authentication state.

type AuthSource

type AuthSource interface {
	EnsureValid(ctx context.Context) (string, error)
}

AuthSource is implemented by AuthController. Bus uses it to fetch a fresh access token for every request (it's cheap when the token is already valid).

type AuthState

type AuthState int32

AuthState represents the current authentication state of the controller.

const (
	AuthLoggedOut AuthState = iota
	AuthLoggingIn
	AuthLoggedIn
	AuthRefreshing
)

func (AuthState) String

func (s AuthState) String() string

type AuthStateChangedMsg

type AuthStateChangedMsg struct{ State AuthState }

Auth

type Bus

type Bus struct {
	// contains filtered or unexported fields
}

func NewBus

func NewBus(cfg BusConfig) *Bus

func (*Bus) AccessToken

func (b *Bus) AccessToken(ctx context.Context) (string, error)

AccessToken exposes Auth.EnsureValid for the SSE consumer, which builds long-lived requests outside of `do`'s code path.

func (*Bus) AttachSession

func (b *Bus) AttachSession(ctx context.Context, sid, mode string) (*AttachResponse, error)

func (*Bus) ExecutorID

func (b *Bus) ExecutorID() string

func (*Bus) FetchExecutorStatus

func (b *Bus) FetchExecutorStatus(ctx context.Context) (*ExecutorStatusResp, error)

func (*Bus) ListSessions

func (b *Bus) ListSessions(ctx context.Context) ([]SessionListItem, error)

func (*Bus) ListWorkspaces added in v0.48.1

func (b *Bus) ListWorkspaces(ctx context.Context) ([]WorkspaceListItem, error)

ListWorkspaces returns all workspaces the authenticated user is a member of. Used at startup when --workspace-id is not provided and no saved executor session matches the current server.

func (*Bus) NewSession

func (b *Bus) NewSession(ctx context.Context, permissionMode string, preferredExecutorID string) (string, error)

func (*Bus) PostCancel

func (b *Bus) PostCancel(ctx context.Context, sid, tid string) error

func (*Bus) PostControl

func (b *Bus) PostControl(ctx context.Context, sid, command string, args map[string]any) (json.RawMessage, error)

func (*Bus) PostDecision

func (b *Bus) PostDecision(ctx context.Context, sid, pid, decision, scope string) error

func (*Bus) PostInbound

func (b *Bus) PostInbound(ctx context.Context, in InboundRequest) (*InboundResponse, error)

PostInbound submits a user prompt. The server creates a session implicitly if SessionID is empty, then claims an active turn (returning 409 if one is already in progress) and asynchronously kicks off cc-broker.

func (*Bus) ServerURL

func (b *Bus) ServerURL() string

func (*Bus) SetExecutorID

func (b *Bus) SetExecutorID(id string)

SetExecutorID updates the executor ID on the Bus. This should only be called during initialization (before any session activity), not concurrently with active requests. Used by tui_run.go when the executor is registered lazily (post-login) and the ID wasn't known at Bus construction time.

func (*Bus) SetWorkspaceID added in v0.48.1

func (b *Bus) SetWorkspaceID(id string)

SetWorkspaceID updates the workspace ID on the Bus. Same caveats as SetExecutorID: only call during init, before any concurrent requests. Used when --workspace-id wasn't provided and the ID is resolved post-login by listing the user's workspaces.

func (*Bus) WorkspaceID added in v0.48.1

func (b *Bus) WorkspaceID() string

type BusConfig

type BusConfig struct {
	ServerURL   string
	WorkspaceID string
	ExecutorID  string
	Auth        AuthSource
	HTTP        *http.Client // optional; defaults to 30s timeout
}

type CancelLoginMsg

type CancelLoginMsg struct{}

CancelLoginMsg is emitted by the login panel when the user presses Esc. The Model converts this to AuthController.CancelLogin().

type CancelReplyMsg

type CancelReplyMsg struct{ Err error }

type ClearRequestedMsg

type ClearRequestedMsg struct{}

type CommandClass

type CommandClass int

CommandClass distinguishes how a slash command is dispatched:

  • LocalClass: handled in-process by the TUI (e.g. /quit).
  • SessionClass: changes which session this TUI is attached to.
  • RemoteClass: forwarded to agentserver /control. The TUI does not interpret these — they're whatever agentserver supports today plus any future R-class command added server-side without TUI changes.
const (
	LocalClass CommandClass = iota
	SessionClass
	RemoteClass
)

type CommandSelectedMsg

type CommandSelectedMsg struct{ Command, Args string }

type ConfirmLogoutMsg

type ConfirmLogoutMsg struct{}

ConfirmLogoutMsg is emitted when the user confirms logout. Model calls AuthController.Logout in response.

type ControlReplyMsg

type ControlReplyMsg struct {
	Command string
	Body    json.RawMessage
	Err     error
}

type DecisionAckMsg

type DecisionAckMsg struct {
	PermissionID string
	Err          error
}

type DeviceCodeReadyMsg

type DeviceCodeReadyMsg struct{ Info LoginInfo }

type EventArrivedMsg

type EventArrivedMsg struct{ Event SSEEvent }

SSE-driven

type ExecutorStatusResp

type ExecutorStatusResp struct {
	ExecutorID    string `json:"executor_id"`
	Status        string `json:"status"`
	LastHeartbeat string `json:"last_heartbeat_at"`
}

type FatalErrorMsg

type FatalErrorMsg struct{ Err error }

Fatal

type InboundAcceptedMsg

type InboundAcceptedMsg struct{ SessionID, TurnID string }

HTTP-driven (Bus replies)

type InboundAttachment

type InboundAttachment struct {
	Kind       string `json:"kind"`
	Filename   string `json:"filename"`
	Size       int    `json:"size"`
	ContentB64 string `json:"content_b64"`
}

func AttachFromPath

func AttachFromPath(path string) (InboundAttachment, error)

AttachFromPath reads a file off disk and returns it as an InboundAttachment ready to drop into the next prompt. Image files (recognised by extension) get kind="image"; everything else is "file". The 8 MiB cap is per-file (the cumulative cap is enforced server-side).

type InboundRejectedMsg

type InboundRejectedMsg struct{ Code, Message string }

type InboundRequest

type InboundRequest struct {
	SessionID           string              `json:"session_id,omitempty"`
	Text                string              `json:"text"`
	Attachments         []InboundAttachment `json:"attachments,omitempty"`
	Metadata            map[string]any      `json:"metadata,omitempty"`
	PermissionResponder bool                `json:"permission_responder,omitempty"`
}

type InboundResponse

type InboundResponse struct {
	SessionID    string `json:"session_id"`
	TurnID       string `json:"turn_id"`
	NextEventSeq int64  `json:"next_event_seq"`
}

type InitialStateMsg

type InitialStateMsg struct {
	SessionID string
	Model     string
	PermMode  string
}

type KeyMap

type KeyMap struct {
	Send        key.Binding
	Newline     key.Binding
	Cancel      key.Binding
	Quit        key.Binding
	SessionPick key.Binding
	Slash       key.Binding
	Help        key.Binding
	PageUp      key.Binding
	PageDown    key.Binding
	Top         key.Binding
	Bottom      key.Binding
}

KeyMap centralises every keybinding so the help screen and Update can share definitions. Add new bindings here, not scattered in handlers.

func NewKeyMap

func NewKeyMap() KeyMap

type ListSessionsReplyMsg

type ListSessionsReplyMsg struct {
	Sessions []SessionListItem
	Err      error
}

type LoginInfo

type LoginInfo struct {
	UserCode      string
	VerifyURL     string
	VerifyURLFull string
	ExpiresIn     int
}

LoginInfo contains the user-visible information from the device authorization flow.

type LoginPollDoneMsg

type LoginPollDoneMsg struct{ Err error }

type LogoutDoneMsg

type LogoutDoneMsg struct{ Err error }

type Mode

type Mode int

Mode tracks which input handler owns the keypress stream right now.

const (
	ModeNormal       Mode = iota // input box has focus
	ModeAwaitPerm                // permission panel is up
	ModeAwaitAskUser             // ask_user panel is up
	ModeAwaitLogin               // OAuth Device Flow panel is up
	ModeAwaitLogout              // logout confirmation panel is up
	ModeCommand                  // slash command palette
	ModeAttachPicker             // file picker for /attach
	ModeQuitting
)

type Model

type Model struct {
	// contains filtered or unexported fields
}

func NewModel

func NewModel(cfg ModelConfig) *Model

func (*Model) Init

func (m *Model) Init() tea.Cmd

func (*Model) InputEnabled

func (m *Model) InputEnabled() bool

func (*Model) SetAuthState

func (m *Model) SetAuthState(s AuthState)

func (*Model) Update

func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update dispatches a Msg to the right handler. The shape:

  1. EventArrivedMsg / AuthStateChangedMsg / panel keys / Send*Msg are handled regardless of mode.
  2. CommandSelectedMsg routes through runCommand (LocalClass / SessionClass / RemoteClass).
  3. Plain KeyMsg in ModeNormal goes to handleNormalKey, then falls through to the textarea if not handled.

func (*Model) View

func (m *Model) View() string

View is a thin wrapper. The actual rendering lives in T13.

type ModelConfig

type ModelConfig struct {
	ServerURL    string
	WorkspaceID  string
	ExecutorID   string
	Bus          *Bus
	Auth         *AuthController
	Yolo         bool
	InitialModel string
	Resume       string
	Continue     bool

	// OnLoggedIn fires when AuthState transitions to LoggedIn. Used to
	// start ExecutorClient + register executor (lazily, post-login).
	// nil if caller doesn't need this signal.
	OnLoggedIn func()

	// OnSessionReady fires whenever the model's sessionID becomes known
	// or changes (Resume on Init, NewSessionReplyMsg, InboundAcceptedMsg
	// for first-prompt-creates-session, ResumeRequestedMsg). Used to start
	// / restart the SSE consumer goroutine. May be called multiple times;
	// implementations should cancel any previous consumer before starting
	// a new one.
	OnSessionReady func(sessionID string)
}

type NewSessionReplyMsg

type NewSessionReplyMsg struct {
	SessionID string
	Err       error
}

type Panel

type Panel interface {
	View(width int) string
	HandleKey(msg tea.KeyMsg) (Panel, tea.Cmd, bool)
	ID() string
}

Panel is the interface implemented by every floating overlay (permission, ask_user, login, logout, etc.). The Model treats panels uniformly: route keys via HandleKey, render via View, dismiss via the boolean.

func NewAskUserPanel

func NewAskUserPanel(in AskUserPanelInput) Panel

func NewLoginPanel

func NewLoginPanel(info LoginInfo) Panel

func NewLogoutPanel

func NewLogoutPanel() Panel

func NewPermissionPanel

func NewPermissionPanel(in PermissionPanelInput) Panel

type ParsedCommand

type ParsedCommand struct {
	Class CommandClass
	Name  string
	Args  string
}

func ParseSlashCommand

func ParseSlashCommand(line string) (ParsedCommand, bool)

ParseSlashCommand classifies a slash-prefixed line. Anything that's neither local nor session falls through to RemoteClass — agentserver decides if it's recognised.

type PermissionPanelInput

type PermissionPanelInput struct {
	PID        string
	Tool       string
	ExecutorID string
	SelfExecID string // local executor's id (for "this machine" tag)
	Args       json.RawMessage
}

type RequeuePermissionMsg

type RequeuePermissionMsg struct{ Panel Panel }

RequeuePermissionMsg is emitted by the permission panel when the user presses Esc ("answer later"). The Model appends the panel to the back of permQueue so the request isn't permanently dismissed.

type ResumeRequestedMsg

type ResumeRequestedMsg struct{ SessionID string }

type SSEConfig

type SSEConfig struct {
	SessionID      string
	InitialBackoff time.Duration // default 1s
	MaxBackoff     time.Duration // default 30s
	HTTP           *http.Client  // optional; defaults to no timeout (SSE is long-lived)
}

SSEConfig configures the consumer's connection + reconnect behavior.

type SSEConsumer

type SSEConsumer struct {
	// contains filtered or unexported fields
}

SSEConsumer streams events from the agentserver SSE endpoint with automatic reconnection. The server-side replay mechanism (Last-Event-ID) resumes from the last received event after a transient failure.

func NewSSEConsumer

func NewSSEConsumer(bus *Bus, cfg SSEConfig) *SSEConsumer

func (*SSEConsumer) Run

func (s *SSEConsumer) Run(ctx context.Context) <-chan SSEEvent

Run starts the consumer. The returned channel emits one event per parsed frame. The channel closes when ctx is cancelled. Reconnection is automatic and transparent to the consumer.

type SSEEvent

type SSEEvent struct {
	Type        string
	Data        []byte
	LastEventID string
}

SSEEvent is one parsed Server-Sent Event frame.

type SSEStatusMsg

type SSEStatusMsg struct {
	Status string
	Reason string

} // live | reconnecting | delayed

type SendAnswerMsg

type SendAnswerMsg struct {
	QID      string
	Selected []string
}

SendAnswerMsg is emitted by the ask_user panel when the user submits. The Model converts this into a Bus.PostAnswer call (endpoint TBD in agent-side; for now the Model can just log it).

type SendDecisionMsg

type SendDecisionMsg struct {
	PID, Verdict, Scope string
}

SendDecisionMsg is emitted by the permission panel when the user picks a verdict. The Model converts this into a Bus.PostDecision call.

type SendPromptMsg

type SendPromptMsg struct {
	Text        string
	Attachments []InboundAttachment
	Metadata    map[string]any
}

Internal user actions

type SessionListItem

type SessionListItem struct {
	SessionID           string  `json:"session_id"`
	ExternalID          string  `json:"external_id"`
	Title               string  `json:"title"`
	LastActivityAt      string  `json:"last_activity_at"`
	PermissionResponder *string `json:"permission_responder"`
}

type StatusTickMsg

type StatusTickMsg struct {
	Tunnel *ExecutorStatusResp
	Err    error
}

Periodic

type Timeline

type Timeline struct {
	// contains filtered or unexported fields
}

Timeline buffers up to `cap` items, indexed by event ID and (for permission_request items) by permission ID so resolutions can find their requests. Render serialises every item into a single multi-line string.

func NewTimeline

func NewTimeline(cap int) *Timeline

func (*Timeline) Append

func (t *Timeline) Append(ev SSEEvent)

Append adds one event. Special-case: permission_resolved updates the matching permission_request's Resolution field rather than adding a row.

func (*Timeline) Len

func (t *Timeline) Len() int

func (*Timeline) Render

func (t *Timeline) Render(_ int, selfExecID string) string

Render produces a single multi-line string ready for the viewport. selfExecID identifies the local executor; tool_use rows tagged with that id render with an "executed locally" marker.

type TimelineItem

type TimelineItem struct {
	EventID    string
	EventType  string
	Payload    json.RawMessage
	Resolution map[string]any // for permission_request items: filled when resolved
}

TimelineItem is one rendered row (or block) in the message stream.

type WorkspaceListItem added in v0.48.1

type WorkspaceListItem struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL