wip: refactoring tui

This commit is contained in:
adamdottv
2025-05-28 12:35:20 -05:00
parent 5e738ce7d3
commit 15d21bf04a
10 changed files with 135 additions and 271 deletions

View File

@@ -18,14 +18,17 @@ import (
)
type App struct {
State map[string]any
CurrentSession *session.Session
Logs interface{} // TODO: Define LogService interface when needed
Logs any // TODO: Define LogService interface when needed
Sessions SessionService
Messages MessageService
History interface{} // TODO: Define HistoryService interface when needed
Permissions interface{} // TODO: Define PermissionService interface when needed
History any // TODO: Define HistoryService interface when needed
Permissions any // TODO: Define PermissionService interface when needed
Status status.Service
Client *client.Client
Client *client.ClientWithResponses
Events *client.Client
PrimaryAgent AgentService
@@ -36,9 +39,9 @@ type App struct {
watcherCancelFuncs []context.CancelFunc
cancelFuncsMutex sync.Mutex
watcherWG sync.WaitGroup
// UI state
filepickerOpen bool
filepickerOpen bool
completionDialogOpen bool
}
@@ -49,16 +52,22 @@ func New(ctx context.Context) (*App, error) {
slog.Error("Failed to initialize status service", "error", err)
return nil, err
}
// Initialize file utilities
fileutil.Init()
// Create HTTP client
httpClient, err := client.NewClient("http://localhost:16713")
url := "http://localhost:16713"
httpClient, err := client.NewClientWithResponses(url)
if err != nil {
slog.Error("Failed to create client", "error", err)
return nil, err
}
eventClient, err := client.NewClient(url)
if err != nil {
slog.Error("Failed to create event client", "error", err)
return nil, err
}
// Create service bridges
sessionBridge := NewSessionServiceBridge(httpClient)
@@ -66,18 +75,20 @@ func New(ctx context.Context) (*App, error) {
agentBridge := NewAgentServiceBridge(httpClient)
app := &App{
State: make(map[string]any),
Client: httpClient,
Events: eventClient,
CurrentSession: &session.Session{},
Sessions: sessionBridge,
Messages: messageBridge,
PrimaryAgent: agentBridge,
Status: status.GetService(),
LSPClients: make(map[string]*lsp.Client),
// TODO: These services need API endpoints:
Logs: nil, // logging.GetService(),
History: nil, // history.GetService(),
Permissions: nil, // permission.GetService(),
Logs: nil, // logging.GetService(),
History: nil, // history.GetService(),
Permissions: nil, // permission.GetService(),
}
// Initialize theme based on configuration

View File

@@ -1,158 +0,0 @@
package app
import (
"encoding/json"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/sst/opencode/internal/message"
"github.com/sst/opencode/pkg/client"
)
// StorageWriteMsg is sent when a storage.write event is received
type StorageWriteMsg struct {
Key string
Content interface{}
}
// ProcessSSEEvent converts SSE events into TUI messages
func ProcessSSEEvent(event interface{}) tea.Msg {
switch e := event.(type) {
case *client.EventStorageWrite:
return StorageWriteMsg{
Key: e.Key,
Content: e.Content,
}
}
// Return the raw event if we don't have a specific handler
return event
}
// MessageFromStorage converts storage content to internal message format
type MessageData struct {
ID string `json:"id"`
Role string `json:"role"`
Parts []interface{} `json:"parts"`
Metadata map[string]interface{} `json:"metadata"`
}
// SessionInfoFromStorage converts storage content to session info
type SessionInfoData struct {
ID string `json:"id"`
Title string `json:"title"`
ShareID *string `json:"shareID,omitempty"`
Tokens struct {
Input float32 `json:"input"`
Output float32 `json:"output"`
Reasoning float32 `json:"reasoning"`
} `json:"tokens"`
}
// ConvertStorageMessage converts a storage message to internal message format
func ConvertStorageMessage(data interface{}, sessionID string) (*message.Message, error) {
// Convert the interface{} to JSON then back to our struct
jsonData, err := json.Marshal(data)
if err != nil {
return nil, err
}
var msgData MessageData
if err := json.Unmarshal(jsonData, &msgData); err != nil {
return nil, err
}
// Convert parts
var parts []message.ContentPart
for _, part := range msgData.Parts {
partMap, ok := part.(map[string]interface{})
if !ok {
continue
}
partType, ok := partMap["type"].(string)
if !ok {
continue
}
switch partType {
case "text":
if text, ok := partMap["text"].(string); ok {
parts = append(parts, message.TextContent{Text: text})
}
case "tool-invocation":
if toolInv, ok := partMap["toolInvocation"].(map[string]interface{}); ok {
// Convert tool invocation to tool call
toolCall := message.ToolCall{
ID: toolInv["toolCallId"].(string),
Name: toolInv["toolName"].(string),
Type: "function",
}
if args, ok := toolInv["args"]; ok {
argsJSON, _ := json.Marshal(args)
toolCall.Input = string(argsJSON)
}
if state, ok := toolInv["state"].(string); ok {
toolCall.Finished = state == "result"
}
parts = append(parts, toolCall)
// If there's a result, add it as a tool result
if result, ok := toolInv["result"]; ok && toolCall.Finished {
resultStr := ""
switch r := result.(type) {
case string:
resultStr = r
default:
resultJSON, _ := json.Marshal(r)
resultStr = string(resultJSON)
}
parts = append(parts, message.ToolResult{
ToolCallID: toolCall.ID,
Name: toolCall.Name,
Content: resultStr,
})
}
}
}
}
// Convert role
var role message.MessageRole
switch msgData.Role {
case "user":
role = message.User
case "assistant":
role = message.Assistant
case "system":
role = message.System
default:
role = message.MessageRole(msgData.Role)
}
// Create message
msg := &message.Message{
ID: msgData.ID,
Role: role,
SessionID: sessionID,
Parts: parts,
CreatedAt: time.Now(), // TODO: Get from metadata
UpdatedAt: time.Now(), // TODO: Get from metadata
}
// Try to get timestamps from metadata
if metadata, ok := msgData.Metadata["time"].(map[string]interface{}); ok {
if created, ok := metadata["created"].(float64); ok {
msg.CreatedAt = time.Unix(int64(created/1000), 0)
}
if completed, ok := metadata["completed"].(float64); ok {
msg.UpdatedAt = time.Unix(int64(completed/1000), 0)
}
}
return msg, nil
}

View File

@@ -14,11 +14,11 @@ import (
// SessionServiceBridge adapts the HTTP API to the old session.Service interface
type SessionServiceBridge struct {
client *client.Client
client *client.ClientWithResponses
}
// NewSessionServiceBridge creates a new session service bridge
func NewSessionServiceBridge(client *client.Client) *SessionServiceBridge {
func NewSessionServiceBridge(client *client.ClientWithResponses) *SessionServiceBridge {
return &SessionServiceBridge{client: client}
}
@@ -107,11 +107,11 @@ func (s *SessionServiceBridge) Delete(ctx context.Context, id string) error {
// AgentServiceBridge provides a minimal agent service that sends messages to the API
type AgentServiceBridge struct {
client *client.Client
client *client.ClientWithResponses
}
// NewAgentServiceBridge creates a new agent service bridge
func NewAgentServiceBridge(client *client.Client) *AgentServiceBridge {
func NewAgentServiceBridge(client *client.ClientWithResponses) *AgentServiceBridge {
return &AgentServiceBridge{client: client}
}
@@ -123,7 +123,7 @@ func (a *AgentServiceBridge) Run(ctx context.Context, sessionID string, text str
// return "", fmt.Errorf("attachments not supported yet")
}
parts := interface{}([]map[string]interface{}{
parts := any([]map[string]any{
{
"type": "text",
"text": text,
@@ -170,12 +170,12 @@ func (a *AgentServiceBridge) CompactSession(ctx context.Context, sessionID strin
// MessageServiceBridge provides a minimal message service that fetches from the API
type MessageServiceBridge struct {
client *client.Client
client *client.ClientWithResponses
broker *pubsub.Broker[message.Message]
}
// NewMessageServiceBridge creates a new message service bridge
func NewMessageServiceBridge(client *client.Client) *MessageServiceBridge {
func NewMessageServiceBridge(client *client.ClientWithResponses) *MessageServiceBridge {
return &MessageServiceBridge{
client: client,
broker: pubsub.NewBroker[message.Message](),
@@ -198,7 +198,7 @@ func (m *MessageServiceBridge) List(ctx context.Context, sessionID string) ([]me
defer resp.Body.Close()
// The API returns a different format, we'll need to adapt it
var rawMessages interface{}
var rawMessages any
if err := json.NewDecoder(resp.Body).Decode(&rawMessages); err != nil {
return nil, err
}
@@ -247,4 +247,5 @@ func (m *MessageServiceBridge) ListAfter(ctx context.Context, sessionID string,
// Subscribe subscribes to message events
func (m *MessageServiceBridge) Subscribe(ctx context.Context) <-chan pubsub.Event[message.Message] {
return m.broker.Subscribe(ctx)
}
}