summaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-05-29 15:37:06 -0500
committeradamdottv <[email protected]>2025-05-29 15:37:06 -0500
commit0e31bbcd9322e1f667b87c88445a4f6effa1d934 (patch)
tree428bbd368a197cb3e80523c629437d8cde86b092 /internal
parent913b3434d8243cc9681a3bf7520e7b027ec3853b (diff)
downloadopencode-0e31bbcd9322e1f667b87c88445a4f6effa1d934.tar.gz
opencode-0e31bbcd9322e1f667b87c88445a4f6effa1d934.zip
wip: refactoring tui
Diffstat (limited to 'internal')
-rw-r--r--internal/message/attachment.go8
-rw-r--r--internal/message/content.go323
-rw-r--r--internal/message/message.go498
-rw-r--r--internal/tui/app/app.go13
-rw-r--r--internal/tui/app/bridge.go87
-rw-r--r--internal/tui/app/interfaces.go20
-rw-r--r--internal/tui/components/chat/chat.go4
-rw-r--r--internal/tui/components/chat/editor.go5
-rw-r--r--internal/tui/components/chat/message.go269
-rw-r--r--internal/tui/components/chat/messages.go71
-rw-r--r--internal/tui/components/dialog/filepicker.go5
-rw-r--r--internal/tui/page/chat.go3
-rw-r--r--internal/tui/tui.go6
13 files changed, 181 insertions, 1131 deletions
diff --git a/internal/message/attachment.go b/internal/message/attachment.go
deleted file mode 100644
index 6e89f0014..000000000
--- a/internal/message/attachment.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package message
-
-type Attachment struct {
- FilePath string
- FileName string
- MimeType string
- Content []byte
-}
diff --git a/internal/message/content.go b/internal/message/content.go
deleted file mode 100644
index 607a43272..000000000
--- a/internal/message/content.go
+++ /dev/null
@@ -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})
-}
diff --git a/internal/message/message.go b/internal/message/message.go
deleted file mode 100644
index 2c05d53bf..000000000
--- a/internal/message/message.go
+++ /dev/null
@@ -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
-}
diff --git a/internal/tui/app/app.go b/internal/tui/app/app.go
index d169f6dc2..3ee0f564d 100644
--- a/internal/tui/app/app.go
+++ b/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)
diff --git a/internal/tui/app/bridge.go b/internal/tui/app/bridge.go
index 74abd712a..3e8768ac1 100644
--- a/internal/tui/app/bridge.go
+++ b/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)
-}
diff --git a/internal/tui/app/interfaces.go b/internal/tui/app/interfaces.go
index 01271156e..4cc9b8022 100644
--- a/internal/tui/app/interfaces.go
+++ b/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
diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go
index 7cd49ef6a..2fabea43d 100644
--- a/internal/tui/components/chat/chat.go
+++ b/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 {
diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go
index c3b06787e..6586f2020 100644
--- a/internal/tui/components/chat/editor.go
+++ b/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)
diff --git a/internal/tui/components/chat/message.go b/internal/tui/components/chat/message.go
index 9f8a9de64..c7b8234a0 100644
--- a/internal/tui/components/chat/message.go
+++ b/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
+// }
diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go
index 92e6ec471..50a0f4200 100644
--- a/internal/tui/components/chat/messages.go
+++ b/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 := ""
diff --git a/internal/tui/components/dialog/filepicker.go b/internal/tui/components/dialog/filepicker.go
index 980ab216b..088e205f2 100644
--- a/internal/tui/components/dialog/filepicker.go
+++ b/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})
}
diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go
index 4a4692bbe..134de6afd 100644
--- a/internal/tui/page/chat.go
+++ b/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)
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index ec4f49fd7..c684b8c8f 100644
--- a/internal/tui/tui.go
+++ b/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)