app.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. package app
  2. import (
  3. "context"
  4. "fmt"
  5. "path/filepath"
  6. "sort"
  7. "log/slog"
  8. tea "github.com/charmbracelet/bubbletea/v2"
  9. "github.com/sst/opencode/internal/commands"
  10. "github.com/sst/opencode/internal/config"
  11. "github.com/sst/opencode/internal/state"
  12. "github.com/sst/opencode/internal/status"
  13. "github.com/sst/opencode/internal/theme"
  14. "github.com/sst/opencode/internal/util"
  15. "github.com/sst/opencode/pkg/client"
  16. )
  17. type App struct {
  18. ConfigPath string
  19. Config *config.Config
  20. Client *client.ClientWithResponses
  21. Provider *client.ProviderInfo
  22. Model *client.ModelInfo
  23. Session *client.SessionInfo
  24. Messages []client.MessageInfo
  25. Status status.Service
  26. Commands commands.Registry
  27. }
  28. type AppInfo struct {
  29. client.AppInfo
  30. Version string
  31. }
  32. var Info AppInfo
  33. func New(ctx context.Context, version string, httpClient *client.ClientWithResponses) (*App, error) {
  34. err := status.InitService()
  35. if err != nil {
  36. slog.Error("Failed to initialize status service", "error", err)
  37. return nil, err
  38. }
  39. appInfoResponse, _ := httpClient.PostAppInfoWithResponse(ctx)
  40. appInfo := appInfoResponse.JSON200
  41. Info = AppInfo{Version: version}
  42. Info.Git = appInfo.Git
  43. Info.Path = appInfo.Path
  44. Info.Time = appInfo.Time
  45. Info.User = appInfo.User
  46. providersResponse, err := httpClient.PostProviderListWithResponse(ctx)
  47. if err != nil {
  48. return nil, err
  49. }
  50. providers := []client.ProviderInfo{}
  51. var defaultProvider *client.ProviderInfo
  52. var defaultModel *client.ModelInfo
  53. var anthropic *client.ProviderInfo
  54. for _, provider := range providersResponse.JSON200.Providers {
  55. if provider.Id == "anthropic" {
  56. anthropic = &provider
  57. }
  58. }
  59. // default to anthropic if available
  60. if anthropic != nil {
  61. defaultProvider = anthropic
  62. defaultModel = getDefaultModel(providersResponse, *anthropic)
  63. }
  64. for _, provider := range providersResponse.JSON200.Providers {
  65. if defaultProvider == nil || defaultModel == nil {
  66. defaultProvider = &provider
  67. defaultModel = getDefaultModel(providersResponse, provider)
  68. }
  69. providers = append(providers, provider)
  70. }
  71. if len(providers) == 0 {
  72. return nil, fmt.Errorf("no providers found")
  73. }
  74. appConfigPath := filepath.Join(Info.Path.Config, "config")
  75. appConfig, err := config.LoadConfig(appConfigPath)
  76. if err != nil {
  77. slog.Info("No TUI config found, using default values", "error", err)
  78. appConfig = config.NewConfig("opencode", defaultProvider.Id, defaultModel.Id)
  79. config.SaveConfig(appConfigPath, appConfig)
  80. }
  81. var currentProvider *client.ProviderInfo
  82. var currentModel *client.ModelInfo
  83. for _, provider := range providers {
  84. if provider.Id == appConfig.Provider {
  85. currentProvider = &provider
  86. for _, model := range provider.Models {
  87. if model.Id == appConfig.Model {
  88. currentModel = &model
  89. }
  90. }
  91. }
  92. }
  93. app := &App{
  94. ConfigPath: appConfigPath,
  95. Config: appConfig,
  96. Client: httpClient,
  97. Provider: currentProvider,
  98. Model: currentModel,
  99. Session: &client.SessionInfo{},
  100. Messages: []client.MessageInfo{},
  101. Status: status.GetService(),
  102. Commands: commands.NewCommandRegistry(),
  103. }
  104. theme.SetTheme(appConfig.Theme)
  105. return app, nil
  106. }
  107. func getDefaultModel(response *client.PostProviderListResponse, provider client.ProviderInfo) *client.ModelInfo {
  108. if match, ok := response.JSON200.Default[provider.Id]; ok {
  109. model := provider.Models[match]
  110. return &model
  111. } else {
  112. for _, model := range provider.Models {
  113. return &model
  114. }
  115. }
  116. return nil
  117. }
  118. type Attachment struct {
  119. FilePath string
  120. FileName string
  121. MimeType string
  122. Content []byte
  123. }
  124. func (a *App) IsBusy() bool {
  125. if len(a.Messages) == 0 {
  126. return false
  127. }
  128. lastMessage := a.Messages[len(a.Messages)-1]
  129. return lastMessage.Metadata.Time.Completed == nil
  130. }
  131. func (a *App) SaveConfig() {
  132. config.SaveConfig(a.ConfigPath, a.Config)
  133. }
  134. func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
  135. cmds := []tea.Cmd{}
  136. session, err := a.CreateSession(ctx)
  137. if err != nil {
  138. status.Error(err.Error())
  139. return nil
  140. }
  141. a.Session = session
  142. cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(session)))
  143. go func() {
  144. // TODO: Handle no provider or model setup, yet
  145. response, err := a.Client.PostSessionInitialize(ctx, client.PostSessionInitializeJSONRequestBody{
  146. SessionID: a.Session.Id,
  147. ProviderID: a.Provider.Id,
  148. ModelID: a.Model.Id,
  149. })
  150. if err != nil {
  151. status.Error(err.Error())
  152. }
  153. if response != nil && response.StatusCode != 200 {
  154. status.Error(fmt.Sprintf("failed to initialize project: %d", response.StatusCode))
  155. }
  156. }()
  157. return tea.Batch(cmds...)
  158. }
  159. func (a *App) MarkProjectInitialized(ctx context.Context) error {
  160. response, err := a.Client.PostAppInitialize(ctx)
  161. if err != nil {
  162. slog.Error("Failed to mark project as initialized", "error", err)
  163. return err
  164. }
  165. if response != nil && response.StatusCode != 200 {
  166. return fmt.Errorf("failed to initialize project: %d", response.StatusCode)
  167. }
  168. return nil
  169. }
  170. func (a *App) CreateSession(ctx context.Context) (*client.SessionInfo, error) {
  171. resp, err := a.Client.PostSessionCreateWithResponse(ctx)
  172. if err != nil {
  173. return nil, err
  174. }
  175. if resp != nil && resp.StatusCode() != 200 {
  176. return nil, fmt.Errorf("failed to create session: %d", resp.StatusCode())
  177. }
  178. session := resp.JSON200
  179. return session, nil
  180. }
  181. func (a *App) SendChatMessage(ctx context.Context, text string, attachments []Attachment) tea.Cmd {
  182. var cmds []tea.Cmd
  183. if a.Session.Id == "" {
  184. session, err := a.CreateSession(ctx)
  185. if err != nil {
  186. status.Error(err.Error())
  187. return nil
  188. }
  189. a.Session = session
  190. cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(session)))
  191. }
  192. // TODO: Handle attachments when API supports them
  193. if len(attachments) > 0 {
  194. // For now, ignore attachments
  195. // return "", fmt.Errorf("attachments not supported yet")
  196. }
  197. part := client.MessagePart{}
  198. part.FromMessagePartText(client.MessagePartText{
  199. Type: "text",
  200. Text: text,
  201. })
  202. parts := []client.MessagePart{part}
  203. go func() {
  204. response, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
  205. SessionID: a.Session.Id,
  206. Parts: parts,
  207. ProviderID: a.Provider.Id,
  208. ModelID: a.Model.Id,
  209. })
  210. if err != nil {
  211. slog.Error("Failed to send message", "error", err)
  212. status.Error(err.Error())
  213. }
  214. if response != nil && response.StatusCode != 200 {
  215. slog.Error("Failed to send message", "error", fmt.Sprintf("failed to send message: %d", response.StatusCode))
  216. status.Error(fmt.Sprintf("failed to send message: %d", response.StatusCode))
  217. }
  218. }()
  219. // The actual response will come through SSE
  220. // For now, just return success
  221. return tea.Batch(cmds...)
  222. }
  223. func (a *App) Cancel(ctx context.Context, sessionID string) error {
  224. response, err := a.Client.PostSessionAbort(ctx, client.PostSessionAbortJSONRequestBody{
  225. SessionID: sessionID,
  226. })
  227. if err != nil {
  228. slog.Error("Failed to cancel session", "error", err)
  229. status.Error(err.Error())
  230. return err
  231. }
  232. if response != nil && response.StatusCode != 200 {
  233. slog.Error("Failed to cancel session", "error", fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
  234. status.Error(fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
  235. return fmt.Errorf("failed to cancel session: %d", response.StatusCode)
  236. }
  237. return nil
  238. }
  239. func (a *App) ListSessions(ctx context.Context) ([]client.SessionInfo, error) {
  240. resp, err := a.Client.PostSessionListWithResponse(ctx)
  241. if err != nil {
  242. return nil, err
  243. }
  244. if resp.StatusCode() != 200 {
  245. return nil, fmt.Errorf("failed to list sessions: %d", resp.StatusCode())
  246. }
  247. if resp.JSON200 == nil {
  248. return []client.SessionInfo{}, nil
  249. }
  250. sessions := *resp.JSON200
  251. sort.Slice(sessions, func(i, j int) bool {
  252. return sessions[i].Time.Created-sessions[j].Time.Created > 0
  253. })
  254. return sessions, nil
  255. }
  256. func (a *App) ListMessages(ctx context.Context, sessionId string) ([]client.MessageInfo, error) {
  257. resp, err := a.Client.PostSessionMessagesWithResponse(ctx, client.PostSessionMessagesJSONRequestBody{SessionID: sessionId})
  258. if err != nil {
  259. return nil, err
  260. }
  261. if resp.StatusCode() != 200 {
  262. return nil, fmt.Errorf("failed to list messages: %d", resp.StatusCode())
  263. }
  264. if resp.JSON200 == nil {
  265. return []client.MessageInfo{}, nil
  266. }
  267. messages := *resp.JSON200
  268. return messages, nil
  269. }
  270. func (a *App) ListProviders(ctx context.Context) ([]client.ProviderInfo, error) {
  271. resp, err := a.Client.PostProviderListWithResponse(ctx)
  272. if err != nil {
  273. return nil, err
  274. }
  275. if resp.StatusCode() != 200 {
  276. return nil, fmt.Errorf("failed to list sessions: %d", resp.StatusCode())
  277. }
  278. if resp.JSON200 == nil {
  279. return []client.ProviderInfo{}, nil
  280. }
  281. providers := *resp.JSON200
  282. return providers.Providers, nil
  283. }