app.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. package app
  2. import (
  3. "context"
  4. "fmt"
  5. "path/filepath"
  6. "sort"
  7. "strings"
  8. "time"
  9. "log/slog"
  10. tea "github.com/charmbracelet/bubbletea/v2"
  11. "github.com/sst/opencode/internal/commands"
  12. "github.com/sst/opencode/internal/components/toast"
  13. "github.com/sst/opencode/internal/config"
  14. "github.com/sst/opencode/internal/styles"
  15. "github.com/sst/opencode/internal/theme"
  16. "github.com/sst/opencode/internal/util"
  17. "github.com/sst/opencode/pkg/client"
  18. )
  19. var RootPath string
  20. type App struct {
  21. Info client.AppInfo
  22. Version string
  23. StatePath string
  24. Config *client.ConfigInfo
  25. Client *client.ClientWithResponses
  26. State *config.State
  27. Provider *client.ProviderInfo
  28. Model *client.ModelInfo
  29. Session *client.SessionInfo
  30. Messages []client.MessageInfo
  31. Commands commands.CommandRegistry
  32. }
  33. type SessionSelectedMsg = *client.SessionInfo
  34. type ModelSelectedMsg struct {
  35. Provider client.ProviderInfo
  36. Model client.ModelInfo
  37. }
  38. type SessionClearedMsg struct{}
  39. type CompactSessionMsg struct{}
  40. type SendMsg struct {
  41. Text string
  42. Attachments []Attachment
  43. }
  44. type CompletionDialogTriggeredMsg struct {
  45. InitialValue string
  46. }
  47. type OptimisticMessageAddedMsg struct {
  48. Message client.MessageInfo
  49. }
  50. func New(
  51. ctx context.Context,
  52. version string,
  53. appInfo client.AppInfo,
  54. httpClient *client.ClientWithResponses,
  55. ) (*App, error) {
  56. RootPath = appInfo.Path.Root
  57. configResponse, err := httpClient.PostConfigGetWithResponse(ctx)
  58. if err != nil {
  59. return nil, err
  60. }
  61. if configResponse.StatusCode() != 200 || configResponse.JSON200 == nil {
  62. return nil, fmt.Errorf("failed to get config: %d", configResponse.StatusCode())
  63. }
  64. configInfo := configResponse.JSON200
  65. if configInfo.Keybinds == nil {
  66. leader := "ctrl+x"
  67. keybinds := client.ConfigKeybinds{
  68. Leader: &leader,
  69. }
  70. configInfo.Keybinds = &keybinds
  71. }
  72. appStatePath := filepath.Join(appInfo.Path.State, "tui")
  73. appState, err := config.LoadState(appStatePath)
  74. if err != nil {
  75. appState = config.NewState()
  76. config.SaveState(appStatePath, appState)
  77. }
  78. if configInfo.Theme != nil {
  79. appState.Theme = *configInfo.Theme
  80. }
  81. if configInfo.Model != nil {
  82. splits := strings.Split(*configInfo.Model, "/")
  83. appState.Provider = splits[0]
  84. appState.Model = strings.Join(splits[1:], "/")
  85. }
  86. // Load themes from all directories
  87. if err := theme.LoadThemesFromDirectories(
  88. appInfo.Path.Config,
  89. appInfo.Path.Root,
  90. appInfo.Path.Cwd,
  91. ); err != nil {
  92. slog.Warn("Failed to load themes from directories", "error", err)
  93. }
  94. if appState.Theme != "" {
  95. if appState.Theme == "system" && styles.Terminal != nil {
  96. theme.UpdateSystemTheme(
  97. styles.Terminal.Background,
  98. styles.Terminal.BackgroundIsDark,
  99. )
  100. }
  101. theme.SetTheme(appState.Theme)
  102. }
  103. slog.Debug("Loaded config", "config", configInfo)
  104. app := &App{
  105. Info: appInfo,
  106. Version: version,
  107. StatePath: appStatePath,
  108. Config: configInfo,
  109. State: appState,
  110. Client: httpClient,
  111. Session: &client.SessionInfo{},
  112. Messages: []client.MessageInfo{},
  113. Commands: commands.LoadFromConfig(configInfo),
  114. }
  115. return app, nil
  116. }
  117. func (a *App) InitializeProvider() tea.Cmd {
  118. return func() tea.Msg {
  119. providersResponse, err := a.Client.PostProviderListWithResponse(context.Background())
  120. if err != nil {
  121. slog.Error("Failed to list providers", "error", err)
  122. // TODO: notify user
  123. return nil
  124. }
  125. if providersResponse != nil && providersResponse.StatusCode() != 200 {
  126. slog.Error("failed to retrieve providers", "status", providersResponse.StatusCode(), "message", string(providersResponse.Body))
  127. return nil
  128. }
  129. providers := []client.ProviderInfo{}
  130. var defaultProvider *client.ProviderInfo
  131. var defaultModel *client.ModelInfo
  132. var anthropic *client.ProviderInfo
  133. for _, provider := range providersResponse.JSON200.Providers {
  134. if provider.Id == "anthropic" {
  135. anthropic = &provider
  136. }
  137. }
  138. // default to anthropic if available
  139. if anthropic != nil {
  140. defaultProvider = anthropic
  141. defaultModel = getDefaultModel(providersResponse, *anthropic)
  142. }
  143. for _, provider := range providersResponse.JSON200.Providers {
  144. if defaultProvider == nil || defaultModel == nil {
  145. defaultProvider = &provider
  146. defaultModel = getDefaultModel(providersResponse, provider)
  147. }
  148. providers = append(providers, provider)
  149. }
  150. if len(providers) == 0 {
  151. slog.Error("No providers configured")
  152. return nil
  153. }
  154. var currentProvider *client.ProviderInfo
  155. var currentModel *client.ModelInfo
  156. for _, provider := range providers {
  157. if provider.Id == a.State.Provider {
  158. currentProvider = &provider
  159. for _, model := range provider.Models {
  160. if model.Id == a.State.Model {
  161. currentModel = &model
  162. }
  163. }
  164. }
  165. }
  166. if currentProvider == nil || currentModel == nil {
  167. currentProvider = defaultProvider
  168. currentModel = defaultModel
  169. }
  170. // TODO: handle no provider or model setup, yet
  171. return ModelSelectedMsg{
  172. Provider: *currentProvider,
  173. Model: *currentModel,
  174. }
  175. }
  176. }
  177. func getDefaultModel(response *client.PostProviderListResponse, provider client.ProviderInfo) *client.ModelInfo {
  178. if match, ok := response.JSON200.Default[provider.Id]; ok {
  179. model := provider.Models[match]
  180. return &model
  181. } else {
  182. for _, model := range provider.Models {
  183. return &model
  184. }
  185. }
  186. return nil
  187. }
  188. type Attachment struct {
  189. FilePath string
  190. FileName string
  191. MimeType string
  192. Content []byte
  193. }
  194. func (a *App) IsBusy() bool {
  195. if len(a.Messages) == 0 {
  196. return false
  197. }
  198. lastMessage := a.Messages[len(a.Messages)-1]
  199. return lastMessage.Metadata.Time.Completed == nil
  200. }
  201. func (a *App) SaveState() {
  202. err := config.SaveState(a.StatePath, a.State)
  203. if err != nil {
  204. slog.Error("Failed to save state", "error", err)
  205. }
  206. }
  207. func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
  208. cmds := []tea.Cmd{}
  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. go func() {
  217. response, err := a.Client.PostSessionInitialize(ctx, client.PostSessionInitializeJSONRequestBody{
  218. SessionID: a.Session.Id,
  219. ProviderID: a.Provider.Id,
  220. ModelID: a.Model.Id,
  221. })
  222. if err != nil {
  223. slog.Error("Failed to initialize project", "error", err)
  224. // status.Error(err.Error())
  225. }
  226. if response != nil && response.StatusCode != 200 {
  227. slog.Error("Failed to initialize project", "error", response.StatusCode)
  228. // status.Error(fmt.Sprintf("failed to initialize project: %d", response.StatusCode))
  229. }
  230. }()
  231. return tea.Batch(cmds...)
  232. }
  233. func (a *App) CompactSession(ctx context.Context) tea.Cmd {
  234. go func() {
  235. response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
  236. SessionID: a.Session.Id,
  237. ProviderID: a.Provider.Id,
  238. ModelID: a.Model.Id,
  239. })
  240. if err != nil {
  241. slog.Error("Failed to compact session", "error", err)
  242. }
  243. if response != nil && response.StatusCode() != 200 {
  244. slog.Error("Failed to compact session", "error", response.StatusCode)
  245. }
  246. }()
  247. return nil
  248. }
  249. func (a *App) MarkProjectInitialized(ctx context.Context) error {
  250. response, err := a.Client.PostAppInitialize(ctx)
  251. if err != nil {
  252. slog.Error("Failed to mark project as initialized", "error", err)
  253. return err
  254. }
  255. if response != nil && response.StatusCode != 200 {
  256. return fmt.Errorf("failed to initialize project: %d", response.StatusCode)
  257. }
  258. return nil
  259. }
  260. func (a *App) CreateSession(ctx context.Context) (*client.SessionInfo, error) {
  261. resp, err := a.Client.PostSessionCreateWithResponse(ctx)
  262. if err != nil {
  263. return nil, err
  264. }
  265. if resp != nil && resp.StatusCode() != 200 {
  266. return nil, fmt.Errorf("failed to create session: %d", resp.StatusCode())
  267. }
  268. session := resp.JSON200
  269. return session, nil
  270. }
  271. func (a *App) SendChatMessage(ctx context.Context, text string, attachments []Attachment) tea.Cmd {
  272. var cmds []tea.Cmd
  273. if a.Session.Id == "" {
  274. session, err := a.CreateSession(ctx)
  275. if err != nil {
  276. return toast.NewErrorToast(err.Error())
  277. }
  278. a.Session = session
  279. cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
  280. }
  281. part := client.MessagePart{}
  282. part.FromMessagePartText(client.MessagePartText{
  283. Type: "text",
  284. Text: text,
  285. })
  286. parts := []client.MessagePart{part}
  287. optimisticMessage := client.MessageInfo{
  288. Id: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
  289. Role: client.User,
  290. Parts: parts,
  291. Metadata: client.MessageMetadata{
  292. SessionID: a.Session.Id,
  293. Time: struct {
  294. Completed *float32 `json:"completed,omitempty"`
  295. Created float32 `json:"created"`
  296. }{
  297. Created: float32(time.Now().Unix()),
  298. },
  299. Tool: make(map[string]client.MessageMetadata_Tool_AdditionalProperties),
  300. },
  301. }
  302. a.Messages = append(a.Messages, optimisticMessage)
  303. cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
  304. cmds = append(cmds, func() tea.Msg {
  305. response, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
  306. SessionID: a.Session.Id,
  307. Parts: parts,
  308. ProviderID: a.Provider.Id,
  309. ModelID: a.Model.Id,
  310. })
  311. if err != nil {
  312. errormsg := fmt.Sprintf("failed to send message: %v", err)
  313. slog.Error(errormsg)
  314. return toast.NewErrorToast(errormsg)()
  315. }
  316. if response != nil && response.StatusCode != 200 {
  317. errormsg := fmt.Sprintf("failed to send message: %d", response.StatusCode)
  318. slog.Error(errormsg)
  319. return toast.NewErrorToast(errormsg)()
  320. }
  321. return nil
  322. })
  323. // The actual response will come through SSE
  324. // For now, just return success
  325. return tea.Batch(cmds...)
  326. }
  327. func (a *App) Cancel(ctx context.Context, sessionID string) error {
  328. response, err := a.Client.PostSessionAbort(ctx, client.PostSessionAbortJSONRequestBody{
  329. SessionID: sessionID,
  330. })
  331. if err != nil {
  332. slog.Error("Failed to cancel session", "error", err)
  333. // status.Error(err.Error())
  334. return err
  335. }
  336. if response != nil && response.StatusCode != 200 {
  337. slog.Error("Failed to cancel session", "error", fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
  338. // status.Error(fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
  339. return fmt.Errorf("failed to cancel session: %d", response.StatusCode)
  340. }
  341. return nil
  342. }
  343. func (a *App) ListSessions(ctx context.Context) ([]client.SessionInfo, error) {
  344. resp, err := a.Client.PostSessionListWithResponse(ctx)
  345. if err != nil {
  346. return nil, err
  347. }
  348. if resp.StatusCode() != 200 {
  349. return nil, fmt.Errorf("failed to list sessions: %d", resp.StatusCode())
  350. }
  351. if resp.JSON200 == nil {
  352. return []client.SessionInfo{}, nil
  353. }
  354. sessions := *resp.JSON200
  355. sort.Slice(sessions, func(i, j int) bool {
  356. return sessions[i].Time.Created-sessions[j].Time.Created > 0
  357. })
  358. return sessions, nil
  359. }
  360. func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
  361. resp, err := a.Client.PostSessionDeleteWithResponse(ctx, client.PostSessionDeleteJSONRequestBody{
  362. SessionID: sessionID,
  363. })
  364. if err != nil {
  365. return err
  366. }
  367. if resp.StatusCode() != 200 {
  368. return fmt.Errorf("failed to delete session: %d", resp.StatusCode())
  369. }
  370. return nil
  371. }
  372. func (a *App) ListMessages(ctx context.Context, sessionId string) ([]client.MessageInfo, error) {
  373. resp, err := a.Client.PostSessionMessagesWithResponse(ctx, client.PostSessionMessagesJSONRequestBody{SessionID: sessionId})
  374. if err != nil {
  375. return nil, err
  376. }
  377. if resp.StatusCode() != 200 {
  378. return nil, fmt.Errorf("failed to list messages: %d", resp.StatusCode())
  379. }
  380. if resp.JSON200 == nil {
  381. return []client.MessageInfo{}, nil
  382. }
  383. messages := *resp.JSON200
  384. return messages, nil
  385. }
  386. func (a *App) ListProviders(ctx context.Context) ([]client.ProviderInfo, error) {
  387. resp, err := a.Client.PostProviderListWithResponse(ctx)
  388. if err != nil {
  389. return nil, err
  390. }
  391. if resp.StatusCode() != 200 {
  392. return nil, fmt.Errorf("failed to list sessions: %d", resp.StatusCode())
  393. }
  394. if resp.JSON200 == nil {
  395. return []client.ProviderInfo{}, nil
  396. }
  397. providers := *resp.JSON200
  398. return providers.Providers, nil
  399. }
  400. // func (a *App) loadCustomKeybinds() {
  401. //
  402. // }