app.go 9.2 KB

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