Browse Source

wip: refactoring tui

adamdottv 10 tháng trước cách đây
mục cha
commit
0e31bbcd93

+ 0 - 8
internal/message/attachment.go

@@ -1,8 +0,0 @@
-package message
-
-type Attachment struct {
-	FilePath string
-	FileName string
-	MimeType string
-	Content  []byte
-}

+ 0 - 323
internal/message/content.go

@@ -1,323 +0,0 @@
-package message
-
-import (
-	"encoding/base64"
-	"slices"
-	"time"
-)
-
-type MessageRole string
-
-const (
-	Assistant MessageRole = "assistant"
-	User      MessageRole = "user"
-	System    MessageRole = "system"
-	Tool      MessageRole = "tool"
-)
-
-type FinishReason string
-
-const (
-	FinishReasonEndTurn          FinishReason = "end_turn"
-	FinishReasonMaxTokens        FinishReason = "max_tokens"
-	FinishReasonToolUse          FinishReason = "tool_use"
-	FinishReasonCanceled         FinishReason = "canceled"
-	FinishReasonError            FinishReason = "error"
-	FinishReasonPermissionDenied FinishReason = "permission_denied"
-
-	// Should never happen
-	FinishReasonUnknown FinishReason = "unknown"
-)
-
-type ContentPart interface {
-	isPart()
-}
-
-type ReasoningContent struct {
-	Thinking string `json:"thinking"`
-}
-
-func (tc ReasoningContent) String() string {
-	return tc.Thinking
-}
-func (ReasoningContent) isPart() {}
-
-type TextContent struct {
-	Text string `json:"text"`
-}
-
-func (tc *TextContent) String() string {
-	if tc == nil {
-		return ""
-	}
-	return tc.Text
-}
-
-func (TextContent) isPart() {}
-
-type ImageURLContent struct {
-	URL    string `json:"url"`
-	Detail string `json:"detail,omitempty"`
-}
-
-func (iuc ImageURLContent) String() string {
-	return iuc.URL
-}
-
-func (ImageURLContent) isPart() {}
-
-type BinaryContent struct {
-	Path     string
-	MIMEType string
-	Data     []byte
-}
-
-func (bc BinaryContent) String(provider string) string {
-	base64Encoded := base64.StdEncoding.EncodeToString(bc.Data)
-	// if provider == models.ProviderOpenAI {
-	// 	return "data:" + bc.MIMEType + ";base64," + base64Encoded
-	// }
-	return base64Encoded
-}
-
-func (BinaryContent) isPart() {}
-
-type ToolCall struct {
-	ID       string `json:"id"`
-	Name     string `json:"name"`
-	Input    string `json:"input"`
-	Type     string `json:"type"`
-	Finished bool   `json:"finished"`
-}
-
-func (ToolCall) isPart() {}
-
-type ToolResult struct {
-	ToolCallID string `json:"tool_call_id"`
-	Name       string `json:"name"`
-	Content    string `json:"content"`
-	Metadata   string `json:"metadata"`
-	IsError    bool   `json:"is_error"`
-}
-
-func (ToolResult) isPart() {}
-
-type Finish struct {
-	Reason FinishReason `json:"reason"`
-	Time   time.Time    `json:"time"`
-}
-
-type DBFinish struct {
-	Reason FinishReason `json:"reason"`
-	Time   int64        `json:"time"`
-}
-
-func (Finish) isPart() {}
-
-func (m *Message) Content() *TextContent {
-	for _, part := range m.Parts {
-		if c, ok := part.(TextContent); ok {
-			return &c
-		}
-	}
-	return nil
-}
-
-func (m *Message) ReasoningContent() ReasoningContent {
-	for _, part := range m.Parts {
-		if c, ok := part.(ReasoningContent); ok {
-			return c
-		}
-	}
-	return ReasoningContent{}
-}
-
-func (m *Message) ImageURLContent() []ImageURLContent {
-	imageURLContents := make([]ImageURLContent, 0)
-	for _, part := range m.Parts {
-		if c, ok := part.(ImageURLContent); ok {
-			imageURLContents = append(imageURLContents, c)
-		}
-	}
-	return imageURLContents
-}
-
-func (m *Message) BinaryContent() []BinaryContent {
-	binaryContents := make([]BinaryContent, 0)
-	for _, part := range m.Parts {
-		if c, ok := part.(BinaryContent); ok {
-			binaryContents = append(binaryContents, c)
-		}
-	}
-	return binaryContents
-}
-
-func (m *Message) ToolCalls() []ToolCall {
-	toolCalls := make([]ToolCall, 0)
-	for _, part := range m.Parts {
-		if c, ok := part.(ToolCall); ok {
-			toolCalls = append(toolCalls, c)
-		}
-	}
-	return toolCalls
-}
-
-func (m *Message) ToolResults() []ToolResult {
-	toolResults := make([]ToolResult, 0)
-	for _, part := range m.Parts {
-		if c, ok := part.(ToolResult); ok {
-			toolResults = append(toolResults, c)
-		}
-	}
-	return toolResults
-}
-
-func (m *Message) IsFinished() bool {
-	for _, part := range m.Parts {
-		if _, ok := part.(Finish); ok {
-			return true
-		}
-	}
-	return false
-}
-
-func (m *Message) FinishPart() *Finish {
-	for _, part := range m.Parts {
-		if c, ok := part.(Finish); ok {
-			return &c
-		}
-	}
-	return nil
-}
-
-func (m *Message) FinishReason() FinishReason {
-	for _, part := range m.Parts {
-		if c, ok := part.(Finish); ok {
-			return c.Reason
-		}
-	}
-	return ""
-}
-
-func (m *Message) IsThinking() bool {
-	if m.ReasoningContent().Thinking != "" && m.Content().Text == "" && !m.IsFinished() {
-		return true
-	}
-	return false
-}
-
-func (m *Message) AppendContent(delta string) {
-	found := false
-	for i, part := range m.Parts {
-		if c, ok := part.(TextContent); ok {
-			m.Parts[i] = TextContent{Text: c.Text + delta}
-			found = true
-		}
-	}
-	if !found {
-		m.Parts = append(m.Parts, TextContent{Text: delta})
-	}
-}
-
-func (m *Message) AppendReasoningContent(delta string) {
-	found := false
-	for i, part := range m.Parts {
-		if c, ok := part.(ReasoningContent); ok {
-			m.Parts[i] = ReasoningContent{Thinking: c.Thinking + delta}
-			found = true
-		}
-	}
-	if !found {
-		m.Parts = append(m.Parts, ReasoningContent{Thinking: delta})
-	}
-}
-
-func (m *Message) FinishToolCall(toolCallID string) {
-	for i, part := range m.Parts {
-		if c, ok := part.(ToolCall); ok {
-			if c.ID == toolCallID {
-				m.Parts[i] = ToolCall{
-					ID:       c.ID,
-					Name:     c.Name,
-					Input:    c.Input,
-					Type:     c.Type,
-					Finished: true,
-				}
-				return
-			}
-		}
-	}
-}
-
-func (m *Message) AppendToolCallInput(toolCallID string, inputDelta string) {
-	for i, part := range m.Parts {
-		if c, ok := part.(ToolCall); ok {
-			if c.ID == toolCallID {
-				m.Parts[i] = ToolCall{
-					ID:       c.ID,
-					Name:     c.Name,
-					Input:    c.Input + inputDelta,
-					Type:     c.Type,
-					Finished: c.Finished,
-				}
-				return
-			}
-		}
-	}
-}
-
-func (m *Message) AddToolCall(tc ToolCall) {
-	for i, part := range m.Parts {
-		if c, ok := part.(ToolCall); ok {
-			if c.ID == tc.ID {
-				m.Parts[i] = tc
-				return
-			}
-		}
-	}
-	m.Parts = append(m.Parts, tc)
-}
-
-func (m *Message) SetToolCalls(tc []ToolCall) {
-	// remove any existing tool call part it could have multiple
-	parts := make([]ContentPart, 0)
-	for _, part := range m.Parts {
-		if _, ok := part.(ToolCall); ok {
-			continue
-		}
-		parts = append(parts, part)
-	}
-	m.Parts = parts
-	for _, toolCall := range tc {
-		m.Parts = append(m.Parts, toolCall)
-	}
-}
-
-func (m *Message) AddToolResult(tr ToolResult) {
-	m.Parts = append(m.Parts, tr)
-}
-
-func (m *Message) SetToolResults(tr []ToolResult) {
-	for _, toolResult := range tr {
-		m.Parts = append(m.Parts, toolResult)
-	}
-}
-
-func (m *Message) AddFinish(reason FinishReason) {
-	// remove any existing finish part
-	for i, part := range m.Parts {
-		if _, ok := part.(Finish); ok {
-			m.Parts = slices.Delete(m.Parts, i, i+1)
-			break
-		}
-	}
-	m.Parts = append(m.Parts, Finish{Reason: reason, Time: time.Now()})
-}
-
-func (m *Message) AddImageURL(url, detail string) {
-	m.Parts = append(m.Parts, ImageURLContent{URL: url, Detail: detail})
-}
-
-func (m *Message) AddBinary(mimeType string, data []byte) {
-	m.Parts = append(m.Parts, BinaryContent{MIMEType: mimeType, Data: data})
-}

+ 0 - 498
internal/message/message.go

@@ -1,498 +0,0 @@
-package message
-
-import (
-	"context"
-	"database/sql"
-	"encoding/json"
-	"fmt"
-	"log/slog"
-	"strings"
-	"sync"
-	"time"
-
-	"github.com/google/uuid"
-	"github.com/sst/opencode/internal/db"
-	"github.com/sst/opencode/internal/pubsub"
-)
-
-type Message struct {
-	ID        string
-	Role      MessageRole
-	SessionID string
-	Parts     []ContentPart
-	CreatedAt time.Time
-	UpdatedAt time.Time
-}
-
-const (
-	EventMessageCreated pubsub.EventType = "message_created"
-	EventMessageUpdated pubsub.EventType = "message_updated"
-	EventMessageDeleted pubsub.EventType = "message_deleted"
-)
-
-type CreateMessageParams struct {
-	Role  MessageRole
-	Parts []ContentPart
-}
-
-type Service interface {
-	pubsub.Subscriber[Message]
-
-	Create(ctx context.Context, sessionID string, params CreateMessageParams) (Message, error)
-	Update(ctx context.Context, message Message) (Message, error)
-	Get(ctx context.Context, id string) (Message, error)
-	List(ctx context.Context, sessionID string) ([]Message, error)
-	ListAfter(ctx context.Context, sessionID string, timestamp time.Time) ([]Message, error)
-	Delete(ctx context.Context, id string) error
-	DeleteSessionMessages(ctx context.Context, sessionID string) error
-}
-
-type service struct {
-	db     *db.Queries
-	broker *pubsub.Broker[Message]
-	mu     sync.RWMutex
-}
-
-var globalMessageService *service
-
-func InitService(dbConn *sql.DB) error {
-	if globalMessageService != nil {
-		return fmt.Errorf("message service already initialized")
-	}
-	queries := db.New(dbConn)
-	broker := pubsub.NewBroker[Message]()
-
-	globalMessageService = &service{
-		db:     queries,
-		broker: broker,
-	}
-	return nil
-}
-
-func GetService() Service {
-	if globalMessageService == nil {
-		panic("message service not initialized. Call message.InitService() first.")
-	}
-	return globalMessageService
-}
-
-func (s *service) Create(ctx context.Context, sessionID string, params CreateMessageParams) (Message, error) {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-
-	isFinished := false
-	for _, p := range params.Parts {
-		if _, ok := p.(Finish); ok {
-			isFinished = true
-			break
-		}
-	}
-	if params.Role == User && !isFinished {
-		params.Parts = append(params.Parts, Finish{Reason: FinishReasonEndTurn, Time: time.Now()})
-	}
-
-	partsJSON, err := marshallParts(params.Parts)
-	if err != nil {
-		return Message{}, fmt.Errorf("failed to marshal message parts: %w", err)
-	}
-
-	dbMsgParams := db.CreateMessageParams{
-		ID:        uuid.New().String(),
-		SessionID: sessionID,
-		Role:      string(params.Role),
-		Parts:     string(partsJSON),
-	}
-
-	dbMessage, err := s.db.CreateMessage(ctx, dbMsgParams)
-	if err != nil {
-		return Message{}, fmt.Errorf("db.CreateMessage: %w", err)
-	}
-
-	message, err := s.fromDBItem(dbMessage)
-	if err != nil {
-		return Message{}, fmt.Errorf("failed to convert DB message: %w", err)
-	}
-
-	s.broker.Publish(EventMessageCreated, message)
-	return message, nil
-}
-
-func (s *service) Update(ctx context.Context, message Message) (Message, error) {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-
-	if message.ID == "" {
-		return Message{}, fmt.Errorf("cannot update message with empty ID")
-	}
-
-	partsJSON, err := marshallParts(message.Parts)
-	if err != nil {
-		return Message{}, fmt.Errorf("failed to marshal message parts for update: %w", err)
-	}
-
-	var dbFinishedAt sql.NullString
-	finishPart := message.FinishPart()
-	if finishPart != nil && !finishPart.Time.IsZero() {
-		dbFinishedAt = sql.NullString{
-			String: finishPart.Time.UTC().Format(time.RFC3339Nano),
-			Valid:  true,
-		}
-	}
-
-	// UpdatedAt is handled by the DB trigger (strftime('%s', 'now'))
-	err = s.db.UpdateMessage(ctx, db.UpdateMessageParams{
-		ID:         message.ID,
-		Parts:      string(partsJSON),
-		FinishedAt: dbFinishedAt,
-	})
-	if err != nil {
-		return Message{}, fmt.Errorf("db.UpdateMessage: %w", err)
-	}
-
-	dbUpdatedMessage, err := s.db.GetMessage(ctx, message.ID)
-	if err != nil {
-		return Message{}, fmt.Errorf("failed to fetch message after update: %w", err)
-	}
-	updatedMessage, err := s.fromDBItem(dbUpdatedMessage)
-	if err != nil {
-		return Message{}, fmt.Errorf("failed to convert updated DB message: %w", err)
-	}
-
-	s.broker.Publish(EventMessageUpdated, updatedMessage)
-	return updatedMessage, nil
-}
-
-func (s *service) Get(ctx context.Context, id string) (Message, error) {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-
-	dbMessage, err := s.db.GetMessage(ctx, id)
-	if err != nil {
-		if err == sql.ErrNoRows {
-			return Message{}, fmt.Errorf("message with ID '%s' not found", id)
-		}
-		return Message{}, fmt.Errorf("db.GetMessage: %w", err)
-	}
-	return s.fromDBItem(dbMessage)
-}
-
-func (s *service) List(ctx context.Context, sessionID string) ([]Message, error) {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-
-	dbMessages, err := s.db.ListMessagesBySession(ctx, sessionID)
-	if err != nil {
-		return nil, fmt.Errorf("db.ListMessagesBySession: %w", err)
-	}
-	messages := make([]Message, len(dbMessages))
-	for i, dbMsg := range dbMessages {
-		msg, convErr := s.fromDBItem(dbMsg)
-		if convErr != nil {
-			return nil, fmt.Errorf("failed to convert DB message at index %d: %w", i, convErr)
-		}
-		messages[i] = msg
-	}
-	return messages, nil
-}
-
-func (s *service) ListAfter(ctx context.Context, sessionID string, timestamp time.Time) ([]Message, error) {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-
-	dbMessages, err := s.db.ListMessagesBySessionAfter(ctx, db.ListMessagesBySessionAfterParams{
-		SessionID: sessionID,
-		CreatedAt: timestamp.Format(time.RFC3339Nano),
-	})
-	if err != nil {
-		return nil, fmt.Errorf("db.ListMessagesBySessionAfter: %w", err)
-	}
-	messages := make([]Message, len(dbMessages))
-	for i, dbMsg := range dbMessages {
-		msg, convErr := s.fromDBItem(dbMsg)
-		if convErr != nil {
-			return nil, fmt.Errorf("failed to convert DB message at index %d (ListAfter): %w", i, convErr)
-		}
-		messages[i] = msg
-	}
-	return messages, nil
-}
-
-func (s *service) Delete(ctx context.Context, id string) error {
-	s.mu.Lock()
-	messageToPublish, err := s.getServiceForPublish(ctx, id)
-	s.mu.Unlock()
-
-	if err != nil {
-		// If error was due to not found, it's not a critical failure for deletion intent
-		if strings.Contains(err.Error(), "not found") {
-			return nil // Or return the error if strictness is required
-		}
-		return err
-	}
-
-	s.mu.Lock()
-	defer s.mu.Unlock()
-	err = s.db.DeleteMessage(ctx, id)
-	if err != nil {
-		return fmt.Errorf("db.DeleteMessage: %w", err)
-	}
-
-	if messageToPublish != nil {
-		s.broker.Publish(EventMessageDeleted, *messageToPublish)
-	}
-	return nil
-}
-
-func (s *service) getServiceForPublish(ctx context.Context, id string) (*Message, error) {
-	dbMsg, err := s.db.GetMessage(ctx, id)
-	if err != nil {
-		return nil, err
-	}
-	msg, convErr := s.fromDBItem(dbMsg)
-	if convErr != nil {
-		return nil, fmt.Errorf("failed to convert DB message for publishing: %w", convErr)
-	}
-	return &msg, nil
-}
-
-func (s *service) DeleteSessionMessages(ctx context.Context, sessionID string) error {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-
-	messagesToDelete, err := s.db.ListMessagesBySession(ctx, sessionID)
-	if err != nil {
-		return fmt.Errorf("failed to list messages for deletion: %w", err)
-	}
-
-	err = s.db.DeleteSessionMessages(ctx, sessionID)
-	if err != nil {
-		return fmt.Errorf("db.DeleteSessionMessages: %w", err)
-	}
-
-	for _, dbMsg := range messagesToDelete {
-		msg, convErr := s.fromDBItem(dbMsg)
-		if convErr == nil {
-			s.broker.Publish(EventMessageDeleted, msg)
-		} else {
-			slog.Error("Failed to convert DB message for delete event publishing", "id", dbMsg.ID, "error", convErr)
-		}
-	}
-	return nil
-}
-
-func (s *service) Subscribe(ctx context.Context) <-chan pubsub.Event[Message] {
-	return s.broker.Subscribe(ctx)
-}
-
-func (s *service) fromDBItem(item db.Message) (Message, error) {
-	parts, err := unmarshallParts([]byte(item.Parts))
-	if err != nil {
-		return Message{}, fmt.Errorf("unmarshallParts for message ID %s: %w. Raw parts: %s", item.ID, err, item.Parts)
-	}
-
-	// Parse timestamps from ISO strings
-	createdAt, err := time.Parse(time.RFC3339Nano, item.CreatedAt)
-	if err != nil {
-		slog.Error("Failed to parse created_at", "value", item.CreatedAt, "error", err)
-		createdAt = time.Now() // Fallback
-	}
-
-	updatedAt, err := time.Parse(time.RFC3339Nano, item.UpdatedAt)
-	if err != nil {
-		slog.Error("Failed to parse created_at", "value", item.CreatedAt, "error", err)
-		updatedAt = time.Now() // Fallback
-	}
-
-	msg := Message{
-		ID:        item.ID,
-		SessionID: item.SessionID,
-		Role:      MessageRole(item.Role),
-		Parts:     parts,
-		CreatedAt: createdAt,
-		UpdatedAt: updatedAt,
-	}
-
-	return msg, nil
-}
-
-func Create(ctx context.Context, sessionID string, params CreateMessageParams) (Message, error) {
-	return GetService().Create(ctx, sessionID, params)
-}
-
-func Update(ctx context.Context, message Message) (Message, error) {
-	return GetService().Update(ctx, message)
-}
-
-func Get(ctx context.Context, id string) (Message, error) {
-	return GetService().Get(ctx, id)
-}
-
-func List(ctx context.Context, sessionID string) ([]Message, error) {
-	return GetService().List(ctx, sessionID)
-}
-
-func ListAfter(ctx context.Context, sessionID string, timestamp time.Time) ([]Message, error) {
-	return GetService().ListAfter(ctx, sessionID, timestamp)
-}
-
-func Delete(ctx context.Context, id string) error {
-	return GetService().Delete(ctx, id)
-}
-
-func DeleteSessionMessages(ctx context.Context, sessionID string) error {
-	return GetService().DeleteSessionMessages(ctx, sessionID)
-}
-
-func Subscribe(ctx context.Context) <-chan pubsub.Event[Message] {
-	return GetService().Subscribe(ctx)
-}
-
-type partType string
-
-const (
-	reasoningType  partType = "reasoning"
-	textType       partType = "text"
-	imageURLType   partType = "image_url"
-	binaryType     partType = "binary"
-	toolCallType   partType = "tool_call"
-	toolResultType partType = "tool_result"
-	finishType     partType = "finish"
-)
-
-type partWrapper struct {
-	Type partType        `json:"type"`
-	Data json.RawMessage `json:"data"`
-}
-
-func marshallParts(parts []ContentPart) ([]byte, error) {
-	wrappedParts := make([]json.RawMessage, len(parts))
-	for i, part := range parts {
-		var typ partType
-		var dataBytes []byte
-		var err error
-
-		switch p := part.(type) {
-		case ReasoningContent:
-			typ = reasoningType
-			dataBytes, err = json.Marshal(p)
-		case TextContent:
-			typ = textType
-			dataBytes, err = json.Marshal(p)
-		case *TextContent:
-			typ = textType
-			dataBytes, err = json.Marshal(p)
-		case ImageURLContent:
-			typ = imageURLType
-			dataBytes, err = json.Marshal(p)
-		case BinaryContent:
-			typ = binaryType
-			dataBytes, err = json.Marshal(p)
-		case ToolCall:
-			typ = toolCallType
-			dataBytes, err = json.Marshal(p)
-		case ToolResult:
-			typ = toolResultType
-			dataBytes, err = json.Marshal(p)
-		case Finish:
-			typ = finishType
-			var dbFinish DBFinish
-			dbFinish.Reason = p.Reason
-			dbFinish.Time = p.Time.UnixMilli()
-			dataBytes, err = json.Marshal(dbFinish)
-		default:
-			return nil, fmt.Errorf("unknown part type for marshalling: %T", part)
-		}
-		if err != nil {
-			return nil, fmt.Errorf("failed to marshal part data for type %s: %w", typ, err)
-		}
-		wrapper := struct {
-			Type partType        `json:"type"`
-			Data json.RawMessage `json:"data"`
-		}{Type: typ, Data: dataBytes}
-		wrappedBytes, err := json.Marshal(wrapper)
-		if err != nil {
-			return nil, fmt.Errorf("failed to marshal part wrapper for type %s: %w", typ, err)
-		}
-		wrappedParts[i] = wrappedBytes
-	}
-	return json.Marshal(wrappedParts)
-}
-
-func unmarshallParts(data []byte) ([]ContentPart, error) {
-	var rawMessages []json.RawMessage
-	if err := json.Unmarshal(data, &rawMessages); err != nil {
-		return nil, fmt.Errorf("failed to unmarshal parts data as array: %w. Data: %s", err, string(data))
-	}
-
-	parts := make([]ContentPart, 0, len(rawMessages))
-	for _, rawPart := range rawMessages {
-		var wrapper partWrapper
-		if err := json.Unmarshal(rawPart, &wrapper); err != nil {
-			// Fallback for old format where parts might be just TextContent string
-			var text string
-			if errText := json.Unmarshal(rawPart, &text); errText == nil {
-				parts = append(parts, TextContent{Text: text})
-				continue
-			}
-			return nil, fmt.Errorf("failed to unmarshal part wrapper: %w. Raw part: %s", err, string(rawPart))
-		}
-
-		switch wrapper.Type {
-		case reasoningType:
-			var p ReasoningContent
-			if err := json.Unmarshal(wrapper.Data, &p); err != nil {
-				return nil, fmt.Errorf("unmarshal ReasoningContent: %w. Data: %s", err, string(wrapper.Data))
-			}
-			parts = append(parts, p)
-		case textType:
-			var p TextContent
-			if err := json.Unmarshal(wrapper.Data, &p); err != nil {
-				return nil, fmt.Errorf("unmarshal TextContent: %w. Data: %s", err, string(wrapper.Data))
-			}
-			parts = append(parts, p)
-		case imageURLType:
-			var p ImageURLContent
-			if err := json.Unmarshal(wrapper.Data, &p); err != nil {
-				return nil, fmt.Errorf("unmarshal ImageURLContent: %w. Data: %s", err, string(wrapper.Data))
-			}
-			parts = append(parts, p)
-		case binaryType:
-			var p BinaryContent
-			if err := json.Unmarshal(wrapper.Data, &p); err != nil {
-				return nil, fmt.Errorf("unmarshal BinaryContent: %w. Data: %s", err, string(wrapper.Data))
-			}
-			parts = append(parts, p)
-		case toolCallType:
-			var p ToolCall
-			if err := json.Unmarshal(wrapper.Data, &p); err != nil {
-				return nil, fmt.Errorf("unmarshal ToolCall: %w. Data: %s", err, string(wrapper.Data))
-			}
-			parts = append(parts, p)
-		case toolResultType:
-			var p ToolResult
-			if err := json.Unmarshal(wrapper.Data, &p); err != nil {
-				return nil, fmt.Errorf("unmarshal ToolResult: %w. Data: %s", err, string(wrapper.Data))
-			}
-			parts = append(parts, p)
-		case finishType:
-			var p DBFinish
-			if err := json.Unmarshal(wrapper.Data, &p); err != nil {
-				return nil, fmt.Errorf("unmarshal Finish: %w. Data: %s", err, string(wrapper.Data))
-			}
-			parts = append(parts, Finish{Reason: FinishReason(p.Reason), Time: time.UnixMilli(p.Time)})
-		default:
-			slog.Warn("Unknown part type during unmarshalling, attempting to parse as TextContent", "type", wrapper.Type, "data", string(wrapper.Data))
-			// Fallback: if type is unknown or empty, try to parse data as TextContent directly
-			var p TextContent
-			if err := json.Unmarshal(wrapper.Data, &p); err == nil {
-				parts = append(parts, p)
-			} else {
-				// If that also fails, log it but continue if possible, or return error
-				slog.Error("Failed to unmarshal unknown part type and fallback to TextContent failed", "type", wrapper.Type, "data", string(wrapper.Data), "error", err)
-				// Depending on strictness, you might return an error here:
-				// return nil, fmt.Errorf("unknown part type '%s' and failed fallback: %w", wrapper.Type, err)
-			}
-		}
-	}
-	return parts, nil
-}

+ 8 - 5
internal/tui/app/app.go

@@ -10,7 +10,6 @@ import (
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/sst/opencode/internal/config"
 	"github.com/sst/opencode/internal/fileutil"
-	"github.com/sst/opencode/internal/message"
 	"github.com/sst/opencode/internal/status"
 	"github.com/sst/opencode/internal/tui/state"
 	"github.com/sst/opencode/internal/tui/theme"
@@ -24,7 +23,6 @@ type App struct {
 	Session  *client.SessionInfo
 	Messages []client.MessageInfo
 
-	MessagesOLD    MessageService
 	LogsOLD        any // TODO: Define LogService interface when needed
 	HistoryOLD     any // TODO: Define HistoryService interface when needed
 	PermissionsOLD any // TODO: Define PermissionService interface when needed
@@ -66,14 +64,12 @@ func New(ctx context.Context) (*App, error) {
 	}
 
 	// Create service bridges
-	messageBridge := NewMessageServiceBridge(httpClient)
 	agentBridge := NewAgentServiceBridge(httpClient)
 
 	app := &App{
 		Client:          httpClient,
 		Events:          eventClient,
 		Session:         &client.SessionInfo{},
-		MessagesOLD:     messageBridge,
 		PrimaryAgentOLD: agentBridge,
 		Status:          status.GetService(),
 
@@ -89,8 +85,15 @@ func New(ctx context.Context) (*App, error) {
 	return app, nil
 }
 
+type Attachment struct {
+	FilePath string
+	FileName string
+	MimeType string
+	Content  []byte
+}
+
 // Create creates a new session
-func (a *App) SendChatMessage(ctx context.Context, text string, attachments []message.Attachment) tea.Cmd {
+func (a *App) SendChatMessage(ctx context.Context, text string, attachments []Attachment) tea.Cmd {
 	var cmds []tea.Cmd
 	if a.Session.Id == "" {
 		resp, err := a.Client.PostSessionCreateWithResponse(ctx)

+ 1 - 86
internal/tui/app/bridge.go

@@ -2,12 +2,8 @@ package app
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
-	"time"
 
-	"github.com/sst/opencode/internal/message"
-	"github.com/sst/opencode/internal/pubsub"
 	"github.com/sst/opencode/pkg/client"
 )
 
@@ -22,7 +18,7 @@ func NewAgentServiceBridge(client *client.ClientWithResponses) *AgentServiceBrid
 }
 
 // Run sends a message to the chat API
-func (a *AgentServiceBridge) Run(ctx context.Context, sessionID string, text string, attachments ...message.Attachment) (string, error) {
+func (a *AgentServiceBridge) Run(ctx context.Context, sessionID string, text string, attachments ...Attachment) (string, error) {
 	// TODO: Handle attachments when API supports them
 	if len(attachments) > 0 {
 		// For now, ignore attachments
@@ -71,84 +67,3 @@ func (a *AgentServiceBridge) CompactSession(ctx context.Context, sessionID strin
 	// TODO: Not implemented in TypeScript API yet
 	return fmt.Errorf("session compaction not implemented in API")
 }
-
-// MessageServiceBridge provides a minimal message service that fetches from the API
-type MessageServiceBridge struct {
-	client *client.ClientWithResponses
-	broker *pubsub.Broker[message.Message]
-}
-
-// NewMessageServiceBridge creates a new message service bridge
-func NewMessageServiceBridge(client *client.ClientWithResponses) *MessageServiceBridge {
-	return &MessageServiceBridge{
-		client: client,
-		broker: pubsub.NewBroker[message.Message](),
-	}
-}
-
-// GetBySession retrieves messages for a session
-func (m *MessageServiceBridge) GetBySession(ctx context.Context, sessionID string) ([]message.Message, error) {
-	return m.List(ctx, sessionID)
-}
-
-// List retrieves messages for a session
-func (m *MessageServiceBridge) List(ctx context.Context, sessionID string) ([]message.Message, error) {
-	resp, err := m.client.PostSessionMessages(ctx, client.PostSessionMessagesJSONRequestBody{
-		SessionID: sessionID,
-	})
-	if err != nil {
-		return nil, err
-	}
-	defer resp.Body.Close()
-
-	// The API returns a different format, we'll need to adapt it
-	var rawMessages any
-	if err := json.NewDecoder(resp.Body).Decode(&rawMessages); err != nil {
-		return nil, err
-	}
-
-	// TODO: Convert the API message format to our internal format
-	// For now, return empty to avoid compilation errors
-	return []message.Message{}, nil
-}
-
-// Create creates a new message - NOT NEEDED, handled by chat API
-func (m *MessageServiceBridge) Create(ctx context.Context, sessionID string, params message.CreateMessageParams) (message.Message, error) {
-	// Messages are created through the chat API
-	return message.Message{}, fmt.Errorf("use chat API to send messages")
-}
-
-// Update updates a message - NOT IMPLEMENTED IN API YET
-func (m *MessageServiceBridge) Update(ctx context.Context, msg message.Message) (message.Message, error) {
-	// TODO: Not implemented in TypeScript API yet
-	return message.Message{}, fmt.Errorf("message update not implemented in API")
-}
-
-// Delete deletes a message - NOT IMPLEMENTED IN API YET
-func (m *MessageServiceBridge) Delete(ctx context.Context, id string) error {
-	// TODO: Not implemented in TypeScript API yet
-	return fmt.Errorf("message delete not implemented in API")
-}
-
-// DeleteSessionMessages deletes all messages for a session - NOT IMPLEMENTED IN API YET
-func (m *MessageServiceBridge) DeleteSessionMessages(ctx context.Context, sessionID string) error {
-	// TODO: Not implemented in TypeScript API yet
-	return fmt.Errorf("delete session messages not implemented in API")
-}
-
-// Get retrieves a message by ID - NOT IMPLEMENTED IN API YET
-func (m *MessageServiceBridge) Get(ctx context.Context, id string) (message.Message, error) {
-	// TODO: Not implemented in TypeScript API yet
-	return message.Message{}, fmt.Errorf("get message by ID not implemented in API")
-}
-
-// ListAfter retrieves messages after a timestamp - NOT IMPLEMENTED IN API YET
-func (m *MessageServiceBridge) ListAfter(ctx context.Context, sessionID string, timestamp time.Time) ([]message.Message, error) {
-	// TODO: Not implemented in TypeScript API yet
-	return []message.Message{}, fmt.Errorf("list messages after timestamp not implemented in API")
-}
-
-// Subscribe subscribes to message events
-func (m *MessageServiceBridge) Subscribe(ctx context.Context) <-chan pubsub.Event[message.Message] {
-	return m.broker.Subscribe(ctx)
-}

+ 1 - 19
internal/tui/app/interfaces.go

@@ -2,29 +2,11 @@ package app
 
 import (
 	"context"
-	"time"
-
-	"github.com/sst/opencode/internal/message"
-	"github.com/sst/opencode/internal/pubsub"
 )
 
-// MessageService defines the interface for message operations
-type MessageService interface {
-	pubsub.Subscriber[message.Message]
-
-	GetBySession(ctx context.Context, sessionID string) ([]message.Message, error)
-	List(ctx context.Context, sessionID string) ([]message.Message, error)
-	Create(ctx context.Context, sessionID string, params message.CreateMessageParams) (message.Message, error)
-	Update(ctx context.Context, msg message.Message) (message.Message, error)
-	Delete(ctx context.Context, id string) error
-	DeleteSessionMessages(ctx context.Context, sessionID string) error
-	Get(ctx context.Context, id string) (message.Message, error)
-	ListAfter(ctx context.Context, sessionID string, timestamp time.Time) ([]message.Message, error)
-}
-
 // AgentService defines the interface for agent operations
 type AgentService interface {
-	Run(ctx context.Context, sessionID string, text string, attachments ...message.Attachment) (string, error)
+	Run(ctx context.Context, sessionID string, text string, attachments ...Attachment) (string, error)
 	Cancel(sessionID string) error
 	IsBusy() bool
 	IsSessionBusy(sessionID string) bool

+ 2 - 2
internal/tui/components/chat/chat.go

@@ -7,7 +7,7 @@ import (
 	"github.com/charmbracelet/lipgloss"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/sst/opencode/internal/config"
-	"github.com/sst/opencode/internal/message"
+	"github.com/sst/opencode/internal/tui/app"
 	"github.com/sst/opencode/internal/tui/styles"
 	"github.com/sst/opencode/internal/tui/theme"
 	"github.com/sst/opencode/internal/version"
@@ -15,7 +15,7 @@ import (
 
 type SendMsg struct {
 	Text        string
-	Attachments []message.Attachment
+	Attachments []app.Attachment
 }
 
 func header(width int) string {

+ 2 - 3
internal/tui/components/chat/editor.go

@@ -13,7 +13,6 @@ import (
 	"github.com/charmbracelet/bubbles/textarea"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
-	"github.com/sst/opencode/internal/message"
 	"github.com/sst/opencode/internal/status"
 	"github.com/sst/opencode/internal/tui/app"
 	"github.com/sst/opencode/internal/tui/components/dialog"
@@ -29,7 +28,7 @@ type editorCmp struct {
 	height         int
 	app            *app.App
 	textarea       textarea.Model
-	attachments    []message.Attachment
+	attachments    []app.Attachment
 	deleteMode     bool
 	history        []string
 	historyIndex   int
@@ -233,7 +232,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 			if len(imageBytes) != 0 {
 				attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments))
-				attachment := message.Attachment{FilePath: attachmentName, FileName: attachmentName, Content: imageBytes, MimeType: "image/png"}
+				attachment := app.Attachment{FilePath: attachmentName, FileName: attachmentName, Content: imageBytes, MimeType: "image/png"}
 				m.attachments = append(m.attachments, attachment)
 			} else {
 				m.textarea.SetValue(m.textarea.Value() + text)

+ 129 - 140
internal/tui/components/chat/message.go

@@ -8,7 +8,6 @@ import (
 	"github.com/charmbracelet/lipgloss"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/sst/opencode/internal/config"
-	"github.com/sst/opencode/internal/message"
 	"github.com/sst/opencode/internal/tui/styles"
 	"github.com/sst/opencode/internal/tui/theme"
 	"github.com/sst/opencode/pkg/client"
@@ -244,17 +243,6 @@ func renderAssistantMessage(
 	return strings.Join(messages, "\n\n")
 }
 
-func findToolResponse(toolCallID string, futureMessages []message.Message) *message.ToolResult {
-	for _, msg := range futureMessages {
-		for _, result := range msg.ToolResults() {
-			if result.ToolCallID == toolCallID {
-				return &result
-			}
-		}
-	}
-	return nil
-}
-
 func renderToolName(name string) string {
 	switch name {
 	// case agent.AgentToolName:
@@ -354,9 +342,9 @@ func removeWorkingDirPrefix(path string) string {
 	return path
 }
 
-func renderToolParams(paramWidth int, toolCall message.ToolCall) string {
+func renderToolParams(paramWidth int, toolCall any) string {
 	params := ""
-	switch toolCall.Name {
+	switch toolCall {
 	// // case agent.AgentToolName:
 	// // 	var params agent.AgentParams
 	// // 	json.Unmarshal([]byte(toolCall.Input), &params)
@@ -445,9 +433,9 @@ func renderToolParams(paramWidth int, toolCall message.ToolCall) string {
 	// 	var params tools.BatchParams
 	// 	json.Unmarshal([]byte(toolCall.Input), &params)
 	// 	return renderParams(paramWidth, fmt.Sprintf("%d parallel calls", len(params.Calls)))
-	default:
-		input := strings.ReplaceAll(toolCall.Input, "\n", " ")
-		params = renderParams(paramWidth, input)
+	// default:
+	// 	input := strings.ReplaceAll(toolCall, "\n", " ")
+	// 	params = renderParams(paramWidth, input)
 	}
 	return params
 }
@@ -460,21 +448,22 @@ func truncateHeight(content string, height int) string {
 	return content
 }
 
-func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string {
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
-
-	if response.IsError {
-		errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
-		errContent = ansi.Truncate(errContent, width-1, "...")
-		return baseStyle.
-			Width(width).
-			Foreground(t.Error()).
-			Render(errContent)
-	}
-
-	resultContent := truncateHeight(response.Content, maxResultHeight)
-	switch toolCall.Name {
+func renderToolResponse(toolCall any, response any, width int) string {
+	return ""
+	// t := theme.CurrentTheme()
+	// baseStyle := styles.BaseStyle()
+	//
+	// if response.IsError {
+	// 	errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
+	// 	errContent = ansi.Truncate(errContent, width-1, "...")
+	// 	return baseStyle.
+	// 		Width(width).
+	// 		Foreground(t.Error()).
+	// 		Render(errContent)
+	// }
+	//
+	// resultContent := truncateHeight(response.Content, maxResultHeight)
+	// switch toolCall.Name {
 	// case agent.AgentToolName:
 	// 	return styles.ForceReplaceBackgroundWithLipgloss(
 	// 		toMarkdown(resultContent, false, width),
@@ -574,113 +563,113 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
 	// 	}
 	//
 	// 	return baseStyle.Width(width).Foreground(t.TextMuted()).Render(strings.Join(toolCalls, "\n\n"))
-	default:
-		resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
-		return styles.ForceReplaceBackgroundWithLipgloss(
-			toMarkdown(resultContent, width),
-			t.Background(),
-		)
-	}
-}
-
-func renderToolMessage(
-	toolCall message.ToolCall,
-	allMessages []message.Message,
-	messagesService message.Service,
-	focusedUIMessageId string,
-	nested bool,
-	width int,
-	position int,
-) string {
-	if nested {
-		width = width - 3
-	}
-
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
-
-	style := baseStyle.
-		Width(width - 1).
-		BorderLeft(true).
-		BorderStyle(lipgloss.ThickBorder()).
-		PaddingLeft(1).
-		BorderForeground(t.TextMuted())
-
-	response := findToolResponse(toolCall.ID, allMessages)
-	toolNameText := baseStyle.Foreground(t.TextMuted()).
-		Render(fmt.Sprintf("%s: ", renderToolName(toolCall.Name)))
-
-	if !toolCall.Finished {
-		// Get a brief description of what the tool is doing
-		toolAction := renderToolAction(toolCall.Name)
-
-		progressText := baseStyle.
-			Width(width - 2 - lipgloss.Width(toolNameText)).
-			Foreground(t.TextMuted()).
-			Render(fmt.Sprintf("%s", toolAction))
-
-		content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
-		return content
-	}
-
-	params := renderToolParams(width-1-lipgloss.Width(toolNameText), toolCall)
-	responseContent := ""
-	if response != nil {
-		responseContent = renderToolResponse(toolCall, *response, width-2)
-		responseContent = strings.TrimSuffix(responseContent, "\n")
-	} else {
-		responseContent = baseStyle.
-			Italic(true).
-			Width(width - 2).
-			Foreground(t.TextMuted()).
-			Render("Waiting for response...")
-	}
-
-	parts := []string{}
-	if !nested {
-		formattedParams := baseStyle.
-			Width(width - 2 - lipgloss.Width(toolNameText)).
-			Foreground(t.TextMuted()).
-			Render(params)
-
-		parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams))
-	} else {
-		prefix := baseStyle.
-			Foreground(t.TextMuted()).
-			Render(" └ ")
-		formattedParams := baseStyle.
-			Width(width - 2 - lipgloss.Width(toolNameText)).
-			Foreground(t.TextMuted()).
-			Render(params)
-		parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
-	}
-
-	// if toolCall.Name == agent.AgentToolName {
-	// 	taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
-	// 	toolCalls := []message.ToolCall{}
-	// 	for _, v := range taskMessages {
-	// 		toolCalls = append(toolCalls, v.ToolCalls()...)
-	// 	}
-	// 	for _, call := range toolCalls {
-	// 		rendered := renderToolMessage(call, []message.Message{}, messagesService, focusedUIMessageId, true, width, 0)
-	// 		parts = append(parts, rendered.content)
-	// 	}
+	// default:
+	// 	resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
+	// 	return styles.ForceReplaceBackgroundWithLipgloss(
+	// 		toMarkdown(resultContent, width),
+	// 		t.Background(),
+	// 	)
 	// }
-	if responseContent != "" && !nested {
-		parts = append(parts, responseContent)
-	}
-
-	content := style.Render(
-		lipgloss.JoinVertical(
-			lipgloss.Left,
-			parts...,
-		),
-	)
-	if nested {
-		content = lipgloss.JoinVertical(
-			lipgloss.Left,
-			parts...,
-		)
-	}
-	return content
 }
+
+// func renderToolMessage(
+// 	toolCall message.ToolCall,
+// 	allMessages []message.Message,
+// 	messagesService message.Service,
+// 	focusedUIMessageId string,
+// 	nested bool,
+// 	width int,
+// 	position int,
+// ) string {
+// 	if nested {
+// 		width = width - 3
+// 	}
+//
+// 	t := theme.CurrentTheme()
+// 	baseStyle := styles.BaseStyle()
+//
+// 	style := baseStyle.
+// 		Width(width - 1).
+// 		BorderLeft(true).
+// 		BorderStyle(lipgloss.ThickBorder()).
+// 		PaddingLeft(1).
+// 		BorderForeground(t.TextMuted())
+//
+// 	response := findToolResponse(toolCall.ID, allMessages)
+// 	toolNameText := baseStyle.Foreground(t.TextMuted()).
+// 		Render(fmt.Sprintf("%s: ", renderToolName(toolCall.Name)))
+//
+// 	if !toolCall.Finished {
+// 		// Get a brief description of what the tool is doing
+// 		toolAction := renderToolAction(toolCall.Name)
+//
+// 		progressText := baseStyle.
+// 			Width(width - 2 - lipgloss.Width(toolNameText)).
+// 			Foreground(t.TextMuted()).
+// 			Render(fmt.Sprintf("%s", toolAction))
+//
+// 		content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
+// 		return content
+// 	}
+//
+// 	params := renderToolParams(width-1-lipgloss.Width(toolNameText), toolCall)
+// 	responseContent := ""
+// 	if response != nil {
+// 		responseContent = renderToolResponse(toolCall, *response, width-2)
+// 		responseContent = strings.TrimSuffix(responseContent, "\n")
+// 	} else {
+// 		responseContent = baseStyle.
+// 			Italic(true).
+// 			Width(width - 2).
+// 			Foreground(t.TextMuted()).
+// 			Render("Waiting for response...")
+// 	}
+//
+// 	parts := []string{}
+// 	if !nested {
+// 		formattedParams := baseStyle.
+// 			Width(width - 2 - lipgloss.Width(toolNameText)).
+// 			Foreground(t.TextMuted()).
+// 			Render(params)
+//
+// 		parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams))
+// 	} else {
+// 		prefix := baseStyle.
+// 			Foreground(t.TextMuted()).
+// 			Render(" └ ")
+// 		formattedParams := baseStyle.
+// 			Width(width - 2 - lipgloss.Width(toolNameText)).
+// 			Foreground(t.TextMuted()).
+// 			Render(params)
+// 		parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
+// 	}
+//
+// 	// if toolCall.Name == agent.AgentToolName {
+// 	// 	taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
+// 	// 	toolCalls := []message.ToolCall{}
+// 	// 	for _, v := range taskMessages {
+// 	// 		toolCalls = append(toolCalls, v.ToolCalls()...)
+// 	// 	}
+// 	// 	for _, call := range toolCalls {
+// 	// 		rendered := renderToolMessage(call, []message.Message{}, messagesService, focusedUIMessageId, true, width, 0)
+// 	// 		parts = append(parts, rendered.content)
+// 	// 	}
+// 	// }
+// 	if responseContent != "" && !nested {
+// 		parts = append(parts, responseContent)
+// 	}
+//
+// 	content := style.Render(
+// 		lipgloss.JoinVertical(
+// 			lipgloss.Left,
+// 			parts...,
+// 		),
+// 	)
+// 	if nested {
+// 		content = lipgloss.JoinVertical(
+// 			lipgloss.Left,
+// 			parts...,
+// 		)
+// 	}
+// 	return content
+// }

+ 35 - 36
internal/tui/components/chat/messages.go

@@ -9,7 +9,6 @@ import (
 	"github.com/charmbracelet/bubbles/viewport"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
-	"github.com/sst/opencode/internal/message"
 	"github.com/sst/opencode/internal/tui/app"
 	"github.com/sst/opencode/internal/tui/components/dialog"
 	"github.com/sst/opencode/internal/tui/state"
@@ -177,41 +176,41 @@ func (m *messagesCmp) View() string {
 		)
 }
 
-func hasToolsWithoutResponse(messages []message.Message) bool {
-	toolCalls := make([]message.ToolCall, 0)
-	toolResults := make([]message.ToolResult, 0)
-	for _, m := range messages {
-		toolCalls = append(toolCalls, m.ToolCalls()...)
-		toolResults = append(toolResults, m.ToolResults()...)
-	}
-
-	for _, v := range toolCalls {
-		found := false
-		for _, r := range toolResults {
-			if v.ID == r.ToolCallID {
-				found = true
-				break
-			}
-		}
-		if !found && v.Finished {
-			return true
-		}
-	}
-	return false
-}
-
-func hasUnfinishedToolCalls(messages []message.Message) bool {
-	toolCalls := make([]message.ToolCall, 0)
-	for _, m := range messages {
-		toolCalls = append(toolCalls, m.ToolCalls()...)
-	}
-	for _, v := range toolCalls {
-		if !v.Finished {
-			return true
-		}
-	}
-	return false
-}
+// func hasToolsWithoutResponse(messages []message.Message) bool {
+// 	toolCalls := make([]message.ToolCall, 0)
+// 	toolResults := make([]message.ToolResult, 0)
+// 	for _, m := range messages {
+// 		toolCalls = append(toolCalls, m.ToolCalls()...)
+// 		toolResults = append(toolResults, m.ToolResults()...)
+// 	}
+//
+// 	for _, v := range toolCalls {
+// 		found := false
+// 		for _, r := range toolResults {
+// 			if v.ID == r.ToolCallID {
+// 				found = true
+// 				break
+// 			}
+// 		}
+// 		if !found && v.Finished {
+// 			return true
+// 		}
+// 	}
+// 	return false
+// }
+
+// func hasUnfinishedToolCalls(messages []message.Message) bool {
+// 	toolCalls := make([]message.ToolCall, 0)
+// 	for _, m := range messages {
+// 		toolCalls = append(toolCalls, m.ToolCalls()...)
+// 	}
+// 	for _, v := range toolCalls {
+// 		if !v.Finished {
+// 			return true
+// 		}
+// 	}
+// 	return false
+// }
 
 func (m *messagesCmp) working() string {
 	text := ""

+ 2 - 3
internal/tui/components/dialog/filepicker.go

@@ -17,7 +17,6 @@ import (
 	"github.com/charmbracelet/bubbles/viewport"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
-	"github.com/sst/opencode/internal/message"
 	"github.com/sst/opencode/internal/status"
 	"github.com/sst/opencode/internal/tui/app"
 	"github.com/sst/opencode/internal/tui/image"
@@ -116,7 +115,7 @@ func (s stack) Pop() (stack, int) {
 }
 
 type AttachmentAddedMsg struct {
-	Attachment message.Attachment
+	Attachment app.Attachment
 }
 
 func (f *filepickerCmp) Init() tea.Cmd {
@@ -269,7 +268,7 @@ func (f *filepickerCmp) addAttachmentToMessage() (tea.Model, tea.Cmd) {
 	mimeBufferSize := min(512, len(content))
 	mimeType := http.DetectContentType(content[:mimeBufferSize])
 	fileName := filepath.Base(selectedFilePath)
-	attachment := message.Attachment{FilePath: selectedFilePath, FileName: fileName, MimeType: mimeType, Content: content}
+	attachment := app.Attachment{FilePath: selectedFilePath, FileName: fileName, MimeType: mimeType, Content: content}
 	f.selectedFile = ""
 	return f, util.CmdHandler(AttachmentAddedMsg{attachment})
 }

+ 1 - 2
internal/tui/page/chat.go

@@ -8,7 +8,6 @@ import (
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
 	"github.com/sst/opencode/internal/completions"
-	"github.com/sst/opencode/internal/message"
 	"github.com/sst/opencode/internal/status"
 	"github.com/sst/opencode/internal/tui/app"
 	"github.com/sst/opencode/internal/tui/components/chat"
@@ -161,7 +160,7 @@ func (p *chatPage) clearSidebar() tea.Cmd {
 	return p.layout.ClearRightPanel()
 }
 
-func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
+func (p *chatPage) sendMessage(text string, attachments []app.Attachment) tea.Cmd {
 	var cmds []tea.Cmd
 	cmd := p.app.SendChatMessage(context.Background(), text, attachments)
 	cmds = append(cmds, cmd)

+ 0 - 6
internal/tui/tui.go

@@ -13,7 +13,6 @@ import (
 	"github.com/sst/opencode/internal/config"
 	"github.com/sst/opencode/internal/tui/app"
 
-	"github.com/sst/opencode/internal/message"
 	"github.com/sst/opencode/internal/permission"
 	"github.com/sst/opencode/internal/pubsub"
 	"github.com/sst/opencode/internal/status"
@@ -574,11 +573,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return a, nil
 		}
 
-	case pubsub.Event[message.Message]:
-		a.pages[page.ChatPage], cmd = a.pages[page.ChatPage].Update(msg)
-		cmds = append(cmds, cmd)
-		return a, tea.Batch(cmds...)
-
 	default:
 		f, filepickerCmd := a.filepicker.Update(msg)
 		a.filepicker = f.(dialog.FilepickerCmp)