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,211 @@
|
||||
// Package protocol implements the correx WebSocket wire contract used to talk
|
||||
// to apps/server over /stream. The server (Kotlin/kotlinx.serialization) uses a
|
||||
// "type" class discriminator. Server->client variants carry @SerialName values
|
||||
// (e.g. "session.started"); client->server variants have no @SerialName, so the
|
||||
// discriminator is the fully-qualified Kotlin class name.
|
||||
package protocol
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// clientPrefix is the kotlinx default serial name for nested ClientMessage
|
||||
// subclasses (qualified class name). The server decodes against this exact value.
|
||||
const clientPrefix = "com.correx.apps.server.protocol.ClientMessage."
|
||||
|
||||
// Server message discriminator values (@SerialName on ServerMessage variants).
|
||||
const (
|
||||
TypeSessionStarted = "session.started"
|
||||
TypeSessionPaused = "session.paused"
|
||||
TypeSessionResumed = "session.resumed"
|
||||
TypeSessionCompleted = "session.completed"
|
||||
TypeSessionFailed = "session.failed"
|
||||
TypeSessionSnapshot = "session_snapshot"
|
||||
TypeStageStarted = "stage.started"
|
||||
TypeStageCompleted = "stage.completed"
|
||||
TypeStageFailed = "stage.failed"
|
||||
TypeInferenceStarted = "inference.started"
|
||||
TypeInferenceDone = "inference.completed"
|
||||
TypeInferenceTimeout = "inference.timed_out"
|
||||
TypeToolStarted = "tool.started"
|
||||
TypeToolCompleted = "tool.completed"
|
||||
TypeToolFailed = "tool.failed"
|
||||
TypeToolRejected = "tool.rejected"
|
||||
TypeApprovalRequired = "approval.required"
|
||||
TypeStageManifest = "stage.tool_manifest"
|
||||
TypeProviderStatus = "provider.status_changed"
|
||||
TypeProtocolError = "protocol_error"
|
||||
TypeArtifactCreated = "artifact.created"
|
||||
TypeRouterResponse = "router.response"
|
||||
TypeSnapshotComplete = "snapshot_complete"
|
||||
TypeWorkflowList = "workflow.list"
|
||||
)
|
||||
|
||||
// ServerMessage is a flat decode of every server->client variant. Field names
|
||||
// across variants don't collide in incompatible JSON types, so a single struct
|
||||
// keyed by Type decodes the whole protocol. Value-class IDs serialize as plain
|
||||
// strings (verified against ServerMessageSerializationTest).
|
||||
type ServerMessage struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
SessionID string `json:"sessionId"`
|
||||
WorkflowID string `json:"workflowId"`
|
||||
StageID string `json:"stageId"`
|
||||
ToolName string `json:"toolName"`
|
||||
Reason string `json:"reason"`
|
||||
Summary string `json:"outputSummary"`
|
||||
Response string `json:"responseText"`
|
||||
Content string `json:"content"`
|
||||
Diff *string `json:"diff"`
|
||||
Tier string `json:"tier"`
|
||||
Message string `json:"message"`
|
||||
RequestID string `json:"requestId"`
|
||||
ArtifactID string `json:"artifactId"`
|
||||
ProviderID string `json:"providerId"`
|
||||
Preview *string `json:"preview"`
|
||||
|
||||
SteeringEmitted bool `json:"steeringEmitted"`
|
||||
OccurredAt int64 `json:"occurredAt"`
|
||||
ElapsedMs int64 `json:"elapsedMs"`
|
||||
LastSequence int64 `json:"lastSequence"`
|
||||
|
||||
State *SessionStateDto `json:"state"`
|
||||
RiskSummary *RiskSummaryDto `json:"riskSummary"`
|
||||
Status *ProviderHealthDto `json:"status"`
|
||||
PendingAppr []ApprovalDto `json:"pendingApprovals"`
|
||||
Tools []ToolRecordDto `json:"tools"`
|
||||
RecentEvents []EventEntryDto `json:"recentEvents"`
|
||||
Stages []StageToolDecl `json:"stages"`
|
||||
Workflows []WorkflowDto `json:"workflows"`
|
||||
}
|
||||
|
||||
type SessionStateDto struct {
|
||||
Status string `json:"status"`
|
||||
CurrentStageID *string `json:"currentStageId"`
|
||||
PauseReason *string `json:"pauseReason"`
|
||||
}
|
||||
|
||||
type RiskSummaryDto struct {
|
||||
Level string `json:"level"`
|
||||
Factors []string `json:"factors"`
|
||||
RecommendedAction string `json:"recommendedAction"`
|
||||
}
|
||||
|
||||
type ProviderHealthDto struct {
|
||||
ProviderID string `json:"providerId"`
|
||||
Status string `json:"status"`
|
||||
LoadPercent *int `json:"loadPercent"`
|
||||
}
|
||||
|
||||
type ApprovalDto struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Tier string `json:"tier"`
|
||||
ToolName *string `json:"toolName"`
|
||||
Preview *string `json:"preview"`
|
||||
}
|
||||
|
||||
type StageToolDecl struct {
|
||||
StageID string `json:"stageId"`
|
||||
Tools []ToolDecl `json:"tools"`
|
||||
}
|
||||
|
||||
type ToolDecl struct {
|
||||
Name string `json:"name"`
|
||||
Tier int `json:"tier"`
|
||||
}
|
||||
|
||||
type EventEntryDto struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
|
||||
type ToolRecordDto struct {
|
||||
Name string `json:"name"`
|
||||
Tier int `json:"tier"`
|
||||
Status string `json:"status"`
|
||||
Diff *string `json:"diff"`
|
||||
}
|
||||
|
||||
type WorkflowDto struct {
|
||||
WorkflowID string `json:"workflowId"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Decode parses one server frame into a ServerMessage.
|
||||
func Decode(raw []byte) (ServerMessage, error) {
|
||||
var m ServerMessage
|
||||
err := json.Unmarshal(raw, &m)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// IsEventBearing reports whether a message carries a session event (and so must
|
||||
// be buffered during the snapshot phase). Control/snapshot messages return false
|
||||
// and are applied immediately.
|
||||
func (m ServerMessage) IsEventBearing() bool {
|
||||
switch m.Type {
|
||||
case TypeSessionStarted, TypeSessionPaused, TypeSessionResumed,
|
||||
TypeSessionCompleted, TypeSessionFailed,
|
||||
TypeStageStarted, TypeStageCompleted, TypeStageFailed,
|
||||
TypeInferenceStarted, TypeInferenceDone, TypeInferenceTimeout,
|
||||
TypeToolStarted, TypeToolCompleted, TypeToolFailed, TypeToolRejected,
|
||||
TypeApprovalRequired, TypeArtifactCreated:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// --- Client -> server encoders ---
|
||||
|
||||
func encode(typeName string, fields map[string]any) []byte {
|
||||
fields["type"] = clientPrefix + typeName
|
||||
b, _ := json.Marshal(fields)
|
||||
return b
|
||||
}
|
||||
|
||||
// StartChatSession opens a new chat session with a client-generated sessionId.
|
||||
func StartChatSession(sessionID, text string) []byte {
|
||||
return encode("StartChatSession", map[string]any{"sessionId": sessionID, "text": text})
|
||||
}
|
||||
|
||||
// ChatInput sends a turn into an existing session. mode is "CHAT" or "STEERING".
|
||||
func ChatInput(sessionID, text, mode string) []byte {
|
||||
return encode("ChatInput", map[string]any{"sessionId": sessionID, "text": text, "mode": mode})
|
||||
}
|
||||
|
||||
// StartSession launches a workflow-backed session.
|
||||
func StartSession(workflowID string) []byte {
|
||||
return encode("StartSession", map[string]any{"workflowId": workflowID, "config": nil})
|
||||
}
|
||||
|
||||
// CancelSession cancels a running session.
|
||||
func CancelSession(sessionID string) []byte {
|
||||
return encode("CancelSession", map[string]any{"sessionId": sessionID})
|
||||
}
|
||||
|
||||
// ApprovalResponse answers an approval gate. decision is "APPROVE" or "REJECT".
|
||||
func ApprovalResponse(requestID, decision string, steeringNote *string) []byte {
|
||||
return encode("ApprovalResponse", map[string]any{
|
||||
"requestId": requestID,
|
||||
"decision": decision,
|
||||
"steeringNote": steeringNote,
|
||||
})
|
||||
}
|
||||
|
||||
// Ping is a keepalive.
|
||||
func Ping(ts int64) []byte {
|
||||
return encode("Ping", map[string]any{"timestamp": ts})
|
||||
}
|
||||
|
||||
// CreateGrant pre-authorizes a tool/tier so future calls skip the approval gate.
|
||||
// scope is "SESSION" or "STAGE"; permittedTiers are tier names like "T3".
|
||||
func CreateGrant(sessionID, scope string, permittedTiers []string, reason, toolName string) []byte {
|
||||
return encode("CreateGrant", map[string]any{
|
||||
"sessionId": sessionID,
|
||||
"scope": scope,
|
||||
"stageId": nil,
|
||||
"permittedTiers": permittedTiers,
|
||||
"reason": reason,
|
||||
"expiresAt": nil,
|
||||
"toolName": toolName,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user