app.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758
  1. package app
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "slices"
  8. "strings"
  9. "log/slog"
  10. tea "github.com/charmbracelet/bubbletea/v2"
  11. "github.com/sst/opencode-sdk-go"
  12. "github.com/sst/opencode/internal/clipboard"
  13. "github.com/sst/opencode/internal/commands"
  14. "github.com/sst/opencode/internal/components/toast"
  15. "github.com/sst/opencode/internal/id"
  16. "github.com/sst/opencode/internal/styles"
  17. "github.com/sst/opencode/internal/theme"
  18. "github.com/sst/opencode/internal/util"
  19. )
  20. type Message struct {
  21. Info opencode.MessageUnion
  22. Parts []opencode.PartUnion
  23. }
  24. type App struct {
  25. Info opencode.App
  26. Agents []opencode.Agent
  27. Providers []opencode.Provider
  28. Version string
  29. StatePath string
  30. Config *opencode.Config
  31. Client *opencode.Client
  32. State *State
  33. AgentIndex int
  34. Provider *opencode.Provider
  35. Model *opencode.Model
  36. Session *opencode.Session
  37. Messages []Message
  38. Permissions []opencode.Permission
  39. CurrentPermission opencode.Permission
  40. Commands commands.CommandRegistry
  41. InitialModel *string
  42. InitialPrompt *string
  43. InitialAgent *string
  44. InitialSession *string
  45. compactCancel context.CancelFunc
  46. IsLeaderSequence bool
  47. }
  48. func (a *App) Agent() *opencode.Agent {
  49. return &a.Agents[a.AgentIndex]
  50. }
  51. type SessionCreatedMsg = struct {
  52. Session *opencode.Session
  53. }
  54. type SessionSelectedMsg = *opencode.Session
  55. type MessageRevertedMsg struct {
  56. Session opencode.Session
  57. Message Message
  58. }
  59. type SessionUnrevertedMsg struct {
  60. Session opencode.Session
  61. }
  62. type SessionLoadedMsg struct{}
  63. type ModelSelectedMsg struct {
  64. Provider opencode.Provider
  65. Model opencode.Model
  66. }
  67. type AgentSelectedMsg struct {
  68. Agent opencode.Agent
  69. }
  70. type SessionClearedMsg struct{}
  71. type CompactSessionMsg struct{}
  72. type SendPrompt = Prompt
  73. type SetEditorContentMsg struct {
  74. Text string
  75. }
  76. type FileRenderedMsg struct {
  77. FilePath string
  78. }
  79. type PermissionRespondedToMsg struct {
  80. Response opencode.SessionPermissionRespondParamsResponse
  81. }
  82. func New(
  83. ctx context.Context,
  84. version string,
  85. appInfo opencode.App,
  86. agents []opencode.Agent,
  87. httpClient *opencode.Client,
  88. initialModel *string,
  89. initialPrompt *string,
  90. initialAgent *string,
  91. initialSession *string,
  92. ) (*App, error) {
  93. util.RootPath = appInfo.Path.Root
  94. util.CwdPath = appInfo.Path.Cwd
  95. configInfo, err := httpClient.Config.Get(ctx)
  96. if err != nil {
  97. return nil, err
  98. }
  99. if configInfo.Keybinds.Leader == "" {
  100. configInfo.Keybinds.Leader = "ctrl+x"
  101. }
  102. appStatePath := filepath.Join(appInfo.Path.State, "tui")
  103. appState, err := LoadState(appStatePath)
  104. if err != nil {
  105. appState = NewState()
  106. SaveState(appStatePath, appState)
  107. }
  108. if appState.AgentModel == nil {
  109. appState.AgentModel = make(map[string]AgentModel)
  110. }
  111. if configInfo.Theme != "" {
  112. appState.Theme = configInfo.Theme
  113. }
  114. themeEnv := os.Getenv("OPENCODE_THEME")
  115. if themeEnv != "" {
  116. appState.Theme = themeEnv
  117. }
  118. agentIndex := slices.IndexFunc(agents, func(a opencode.Agent) bool {
  119. return a.Mode != "subagent"
  120. })
  121. var agent *opencode.Agent
  122. modeName := "build"
  123. if appState.Agent != "" {
  124. modeName = appState.Agent
  125. }
  126. if initialAgent != nil && *initialAgent != "" {
  127. modeName = *initialAgent
  128. }
  129. for i, m := range agents {
  130. if m.Name == modeName {
  131. agentIndex = i
  132. break
  133. }
  134. }
  135. agent = &agents[agentIndex]
  136. if agent.Model.ModelID != "" {
  137. appState.AgentModel[agent.Name] = AgentModel{
  138. ProviderID: agent.Model.ProviderID,
  139. ModelID: agent.Model.ModelID,
  140. }
  141. }
  142. if err := theme.LoadThemesFromDirectories(
  143. appInfo.Path.Config,
  144. appInfo.Path.Root,
  145. appInfo.Path.Cwd,
  146. ); err != nil {
  147. slog.Warn("Failed to load themes from directories", "error", err)
  148. }
  149. if appState.Theme != "" {
  150. if appState.Theme == "system" && styles.Terminal != nil {
  151. theme.UpdateSystemTheme(
  152. styles.Terminal.Background,
  153. styles.Terminal.BackgroundIsDark,
  154. )
  155. }
  156. theme.SetTheme(appState.Theme)
  157. }
  158. slog.Debug("Loaded config", "config", configInfo)
  159. app := &App{
  160. Info: appInfo,
  161. Agents: agents,
  162. Version: version,
  163. StatePath: appStatePath,
  164. Config: configInfo,
  165. State: appState,
  166. Client: httpClient,
  167. AgentIndex: agentIndex,
  168. Session: &opencode.Session{},
  169. Messages: []Message{},
  170. Commands: commands.LoadFromConfig(configInfo),
  171. InitialModel: initialModel,
  172. InitialPrompt: initialPrompt,
  173. InitialAgent: initialAgent,
  174. InitialSession: initialSession,
  175. }
  176. return app, nil
  177. }
  178. func (a *App) Keybind(commandName commands.CommandName) string {
  179. command := a.Commands[commandName]
  180. kb := command.Keybindings[0]
  181. key := kb.Key
  182. if kb.RequiresLeader {
  183. key = a.Config.Keybinds.Leader + " " + kb.Key
  184. }
  185. return key
  186. }
  187. func (a *App) Key(commandName commands.CommandName) string {
  188. t := theme.CurrentTheme()
  189. base := styles.NewStyle().Background(t.Background()).Foreground(t.Text()).Bold(true).Render
  190. muted := styles.NewStyle().
  191. Background(t.Background()).
  192. Foreground(t.TextMuted()).
  193. Faint(true).
  194. Render
  195. command := a.Commands[commandName]
  196. key := a.Keybind(commandName)
  197. return base(key) + muted(" "+command.Description)
  198. }
  199. func SetClipboard(text string) tea.Cmd {
  200. var cmds []tea.Cmd
  201. cmds = append(cmds, func() tea.Msg {
  202. clipboard.Write(clipboard.FmtText, []byte(text))
  203. return nil
  204. })
  205. // try to set the clipboard using OSC52 for terminals that support it
  206. cmds = append(cmds, tea.SetClipboard(text))
  207. return tea.Sequence(cmds...)
  208. }
  209. func (a *App) cycleMode(forward bool) (*App, tea.Cmd) {
  210. if forward {
  211. a.AgentIndex++
  212. if a.AgentIndex >= len(a.Agents) {
  213. a.AgentIndex = 0
  214. }
  215. } else {
  216. a.AgentIndex--
  217. if a.AgentIndex < 0 {
  218. a.AgentIndex = len(a.Agents) - 1
  219. }
  220. }
  221. if a.Agent().Mode == "subagent" {
  222. return a.cycleMode(forward)
  223. }
  224. modelID := a.Agent().Model.ModelID
  225. providerID := a.Agent().Model.ProviderID
  226. if modelID == "" {
  227. if model, ok := a.State.AgentModel[a.Agent().Name]; ok {
  228. modelID = model.ModelID
  229. providerID = model.ProviderID
  230. }
  231. }
  232. if modelID != "" {
  233. for _, provider := range a.Providers {
  234. if provider.ID == providerID {
  235. a.Provider = &provider
  236. for _, model := range provider.Models {
  237. if model.ID == modelID {
  238. a.Model = &model
  239. break
  240. }
  241. }
  242. break
  243. }
  244. }
  245. }
  246. a.State.Agent = a.Agent().Name
  247. return a, a.SaveState()
  248. }
  249. func (a *App) SwitchAgent() (*App, tea.Cmd) {
  250. return a.cycleMode(true)
  251. }
  252. func (a *App) SwitchAgentReverse() (*App, tea.Cmd) {
  253. return a.cycleMode(false)
  254. }
  255. func (a *App) CycleRecentModel() (*App, tea.Cmd) {
  256. recentModels := a.State.RecentlyUsedModels
  257. if len(recentModels) > 5 {
  258. recentModels = recentModels[:5]
  259. }
  260. if len(recentModels) < 2 {
  261. return a, toast.NewInfoToast("Need at least 2 recent models to cycle")
  262. }
  263. nextIndex := 0
  264. for i, recentModel := range recentModels {
  265. if a.Provider != nil && a.Model != nil && recentModel.ProviderID == a.Provider.ID && recentModel.ModelID == a.Model.ID {
  266. nextIndex = (i + 1) % len(recentModels)
  267. break
  268. }
  269. }
  270. for range recentModels {
  271. currentRecentModel := recentModels[nextIndex%len(recentModels)]
  272. provider, model := findModelByProviderAndModelID(a.Providers, currentRecentModel.ProviderID, currentRecentModel.ModelID)
  273. if provider != nil && model != nil {
  274. a.Provider, a.Model = provider, model
  275. a.State.AgentModel[a.Agent().Name] = AgentModel{ProviderID: provider.ID, ModelID: model.ID}
  276. return a, tea.Sequence(a.SaveState(), toast.NewSuccessToast(fmt.Sprintf("Switched to %s (%s)", model.Name, provider.Name)))
  277. }
  278. recentModels = append(recentModels[:nextIndex%len(recentModels)], recentModels[nextIndex%len(recentModels)+1:]...)
  279. if len(recentModels) < 2 {
  280. a.State.RecentlyUsedModels = recentModels
  281. return a, tea.Sequence(a.SaveState(), toast.NewInfoToast("Not enough valid recent models to cycle"))
  282. }
  283. }
  284. a.State.RecentlyUsedModels = recentModels
  285. return a, toast.NewErrorToast("Recent model not found")
  286. }
  287. // findModelByFullID finds a model by its full ID in the format "provider/model"
  288. func findModelByFullID(
  289. providers []opencode.Provider,
  290. fullModelID string,
  291. ) (*opencode.Provider, *opencode.Model) {
  292. modelParts := strings.SplitN(fullModelID, "/", 2)
  293. if len(modelParts) < 2 {
  294. return nil, nil
  295. }
  296. providerID := modelParts[0]
  297. modelID := modelParts[1]
  298. return findModelByProviderAndModelID(providers, providerID, modelID)
  299. }
  300. // findModelByProviderAndModelID finds a model by provider ID and model ID
  301. func findModelByProviderAndModelID(
  302. providers []opencode.Provider,
  303. providerID, modelID string,
  304. ) (*opencode.Provider, *opencode.Model) {
  305. for _, provider := range providers {
  306. if provider.ID != providerID {
  307. continue
  308. }
  309. for _, model := range provider.Models {
  310. if model.ID == modelID {
  311. return &provider, &model
  312. }
  313. }
  314. // Provider found but model not found
  315. return nil, nil
  316. }
  317. // Provider not found
  318. return nil, nil
  319. }
  320. // findProviderByID finds a provider by its ID
  321. func findProviderByID(providers []opencode.Provider, providerID string) *opencode.Provider {
  322. for _, provider := range providers {
  323. if provider.ID == providerID {
  324. return &provider
  325. }
  326. }
  327. return nil
  328. }
  329. func (a *App) InitializeProvider() tea.Cmd {
  330. providersResponse, err := a.Client.App.Providers(context.Background())
  331. if err != nil {
  332. slog.Error("Failed to list providers", "error", err)
  333. // TODO: notify user
  334. return nil
  335. }
  336. providers := providersResponse.Providers
  337. if len(providers) == 0 {
  338. slog.Error("No providers configured")
  339. return nil
  340. }
  341. a.Providers = providers
  342. // retains backwards compatibility with old state format
  343. if model, ok := a.State.AgentModel[a.State.Agent]; ok {
  344. a.State.Provider = model.ProviderID
  345. a.State.Model = model.ModelID
  346. }
  347. var selectedProvider *opencode.Provider
  348. var selectedModel *opencode.Model
  349. // Priority 1: Command line --model flag (InitialModel)
  350. if a.InitialModel != nil && *a.InitialModel != "" {
  351. if provider, model := findModelByFullID(providers, *a.InitialModel); provider != nil &&
  352. model != nil {
  353. selectedProvider = provider
  354. selectedModel = model
  355. slog.Debug(
  356. "Selected model from command line",
  357. "provider",
  358. provider.ID,
  359. "model",
  360. model.ID,
  361. )
  362. } else {
  363. slog.Debug("Command line model not found", "model", *a.InitialModel)
  364. }
  365. }
  366. // Priority 2: Config file model setting
  367. if selectedProvider == nil && a.Config.Model != "" {
  368. if provider, model := findModelByFullID(providers, a.Config.Model); provider != nil &&
  369. model != nil {
  370. selectedProvider = provider
  371. selectedModel = model
  372. slog.Debug("Selected model from config", "provider", provider.ID, "model", model.ID)
  373. } else {
  374. slog.Debug("Config model not found", "model", a.Config.Model)
  375. }
  376. }
  377. // Priority 3: Current agent's preferred model
  378. if selectedProvider == nil && a.Agent().Model.ModelID != "" {
  379. if provider, model := findModelByProviderAndModelID(providers, a.Agent().Model.ProviderID, a.Agent().Model.ModelID); provider != nil && model != nil {
  380. selectedProvider = provider
  381. selectedModel = model
  382. slog.Debug("Selected model from current agent", "provider", provider.ID, "model", model.ID, "agent", a.Agent().Name)
  383. } else {
  384. slog.Debug("Agent model not found", "provider", a.Agent().Model.ProviderID, "model", a.Agent().Model.ModelID, "agent", a.Agent().Name)
  385. }
  386. }
  387. // Priority 4: Recent model usage (most recently used model)
  388. if selectedProvider == nil && len(a.State.RecentlyUsedModels) > 0 {
  389. recentUsage := a.State.RecentlyUsedModels[0] // Most recent is first
  390. if provider, model := findModelByProviderAndModelID(providers, recentUsage.ProviderID, recentUsage.ModelID); provider != nil &&
  391. model != nil {
  392. selectedProvider = provider
  393. selectedModel = model
  394. slog.Debug(
  395. "Selected model from recent usage",
  396. "provider",
  397. provider.ID,
  398. "model",
  399. model.ID,
  400. )
  401. } else {
  402. slog.Debug("Recent model not found", "provider", recentUsage.ProviderID, "model", recentUsage.ModelID)
  403. }
  404. }
  405. // Priority 5: State-based model (backwards compatibility)
  406. if selectedProvider == nil && a.State.Provider != "" && a.State.Model != "" {
  407. if provider, model := findModelByProviderAndModelID(providers, a.State.Provider, a.State.Model); provider != nil &&
  408. model != nil {
  409. selectedProvider = provider
  410. selectedModel = model
  411. slog.Debug("Selected model from state", "provider", provider.ID, "model", model.ID)
  412. } else {
  413. slog.Debug("State model not found", "provider", a.State.Provider, "model", a.State.Model)
  414. }
  415. }
  416. // Priority 6: Internal priority fallback (Anthropic preferred, then first available)
  417. if selectedProvider == nil {
  418. // Try Anthropic first as internal priority
  419. if provider := findProviderByID(providers, "anthropic"); provider != nil {
  420. if model := getDefaultModel(providersResponse, *provider); model != nil {
  421. selectedProvider = provider
  422. selectedModel = model
  423. slog.Debug(
  424. "Selected model from internal priority (Anthropic)",
  425. "provider",
  426. provider.ID,
  427. "model",
  428. model.ID,
  429. )
  430. }
  431. }
  432. // If Anthropic not available, use first available provider
  433. if selectedProvider == nil && len(providers) > 0 {
  434. provider := &providers[0]
  435. if model := getDefaultModel(providersResponse, *provider); model != nil {
  436. selectedProvider = provider
  437. selectedModel = model
  438. slog.Debug(
  439. "Selected model from fallback (first available)",
  440. "provider",
  441. provider.ID,
  442. "model",
  443. model.ID,
  444. )
  445. }
  446. }
  447. }
  448. // Final safety check
  449. if selectedProvider == nil || selectedModel == nil {
  450. slog.Error("Failed to select any model")
  451. return nil
  452. }
  453. var cmds []tea.Cmd
  454. cmds = append(cmds, util.CmdHandler(ModelSelectedMsg{
  455. Provider: *selectedProvider,
  456. Model: *selectedModel,
  457. }))
  458. // Load initial session if provided
  459. if a.InitialSession != nil && *a.InitialSession != "" {
  460. cmds = append(cmds, func() tea.Msg {
  461. // Find the session by ID
  462. sessions, err := a.ListSessions(context.Background())
  463. if err != nil {
  464. slog.Error("Failed to list sessions for initial session", "error", err)
  465. return toast.NewErrorToast("Failed to load initial session")()
  466. }
  467. for _, session := range sessions {
  468. if session.ID == *a.InitialSession {
  469. return SessionSelectedMsg(&session)
  470. }
  471. }
  472. slog.Warn("Initial session not found", "sessionID", *a.InitialSession)
  473. return toast.NewErrorToast("Session not found: " + *a.InitialSession)()
  474. })
  475. }
  476. if a.InitialPrompt != nil && *a.InitialPrompt != "" {
  477. cmds = append(cmds, util.CmdHandler(SendPrompt{Text: *a.InitialPrompt}))
  478. }
  479. return tea.Sequence(cmds...)
  480. }
  481. func getDefaultModel(
  482. response *opencode.AppProvidersResponse,
  483. provider opencode.Provider,
  484. ) *opencode.Model {
  485. if match, ok := response.Default[provider.ID]; ok {
  486. model := provider.Models[match]
  487. return &model
  488. } else {
  489. for _, model := range provider.Models {
  490. return &model
  491. }
  492. }
  493. return nil
  494. }
  495. func (a *App) IsBusy() bool {
  496. if len(a.Messages) == 0 {
  497. return false
  498. }
  499. lastMessage := a.Messages[len(a.Messages)-1]
  500. if casted, ok := lastMessage.Info.(opencode.AssistantMessage); ok {
  501. return casted.Time.Completed == 0
  502. }
  503. return true
  504. }
  505. func (a *App) SaveState() tea.Cmd {
  506. return func() tea.Msg {
  507. err := SaveState(a.StatePath, a.State)
  508. if err != nil {
  509. slog.Error("Failed to save state", "error", err)
  510. }
  511. return nil
  512. }
  513. }
  514. func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
  515. cmds := []tea.Cmd{}
  516. session, err := a.CreateSession(ctx)
  517. if err != nil {
  518. // status.Error(err.Error())
  519. return nil
  520. }
  521. a.Session = session
  522. cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
  523. go func() {
  524. _, err := a.Client.Session.Init(ctx, a.Session.ID, opencode.SessionInitParams{
  525. MessageID: opencode.F(id.Ascending(id.Message)),
  526. ProviderID: opencode.F(a.Provider.ID),
  527. ModelID: opencode.F(a.Model.ID),
  528. })
  529. if err != nil {
  530. slog.Error("Failed to initialize project", "error", err)
  531. // status.Error(err.Error())
  532. }
  533. }()
  534. return tea.Batch(cmds...)
  535. }
  536. func (a *App) CompactSession(ctx context.Context) tea.Cmd {
  537. if a.compactCancel != nil {
  538. a.compactCancel()
  539. }
  540. compactCtx, cancel := context.WithCancel(ctx)
  541. a.compactCancel = cancel
  542. go func() {
  543. defer func() {
  544. a.compactCancel = nil
  545. }()
  546. _, err := a.Client.Session.Summarize(
  547. compactCtx,
  548. a.Session.ID,
  549. opencode.SessionSummarizeParams{
  550. ProviderID: opencode.F(a.Provider.ID),
  551. ModelID: opencode.F(a.Model.ID),
  552. },
  553. )
  554. if err != nil {
  555. if compactCtx.Err() != context.Canceled {
  556. slog.Error("Failed to compact session", "error", err)
  557. }
  558. }
  559. }()
  560. return nil
  561. }
  562. func (a *App) MarkProjectInitialized(ctx context.Context) error {
  563. _, err := a.Client.App.Init(ctx)
  564. if err != nil {
  565. slog.Error("Failed to mark project as initialized", "error", err)
  566. return err
  567. }
  568. return nil
  569. }
  570. func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) {
  571. session, err := a.Client.Session.New(ctx)
  572. if err != nil {
  573. return nil, err
  574. }
  575. return session, nil
  576. }
  577. func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) {
  578. var cmds []tea.Cmd
  579. if a.Session.ID == "" {
  580. session, err := a.CreateSession(ctx)
  581. if err != nil {
  582. return a, toast.NewErrorToast(err.Error())
  583. }
  584. a.Session = session
  585. cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
  586. }
  587. messageID := id.Ascending(id.Message)
  588. message := prompt.ToMessage(messageID, a.Session.ID)
  589. a.Messages = append(a.Messages, message)
  590. cmds = append(cmds, func() tea.Msg {
  591. _, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{
  592. ProviderID: opencode.F(a.Provider.ID),
  593. ModelID: opencode.F(a.Model.ID),
  594. Agent: opencode.F(a.Agent().Name),
  595. MessageID: opencode.F(messageID),
  596. Parts: opencode.F(message.ToSessionChatParams()),
  597. })
  598. if err != nil {
  599. errormsg := fmt.Sprintf("failed to send message: %v", err)
  600. slog.Error(errormsg)
  601. return toast.NewErrorToast(errormsg)()
  602. }
  603. return nil
  604. })
  605. // The actual response will come through SSE
  606. // For now, just return success
  607. return a, tea.Batch(cmds...)
  608. }
  609. func (a *App) Cancel(ctx context.Context, sessionID string) error {
  610. // Cancel any running compact operation
  611. if a.compactCancel != nil {
  612. a.compactCancel()
  613. a.compactCancel = nil
  614. }
  615. _, err := a.Client.Session.Abort(ctx, sessionID)
  616. if err != nil {
  617. slog.Error("Failed to cancel session", "error", err)
  618. return err
  619. }
  620. return nil
  621. }
  622. func (a *App) ListSessions(ctx context.Context) ([]opencode.Session, error) {
  623. response, err := a.Client.Session.List(ctx)
  624. if err != nil {
  625. return nil, err
  626. }
  627. if response == nil {
  628. return []opencode.Session{}, nil
  629. }
  630. sessions := *response
  631. return sessions, nil
  632. }
  633. func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
  634. _, err := a.Client.Session.Delete(ctx, sessionID)
  635. if err != nil {
  636. slog.Error("Failed to delete session", "error", err)
  637. return err
  638. }
  639. return nil
  640. }
  641. func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, error) {
  642. response, err := a.Client.Session.Messages(ctx, sessionId)
  643. if err != nil {
  644. return nil, err
  645. }
  646. if response == nil {
  647. return []Message{}, nil
  648. }
  649. messages := []Message{}
  650. for _, message := range *response {
  651. msg := Message{
  652. Info: message.Info.AsUnion(),
  653. Parts: []opencode.PartUnion{},
  654. }
  655. for _, part := range message.Parts {
  656. msg.Parts = append(msg.Parts, part.AsUnion())
  657. }
  658. messages = append(messages, msg)
  659. }
  660. return messages, nil
  661. }
  662. func (a *App) ListProviders(ctx context.Context) ([]opencode.Provider, error) {
  663. response, err := a.Client.App.Providers(ctx)
  664. if err != nil {
  665. return nil, err
  666. }
  667. if response == nil {
  668. return []opencode.Provider{}, nil
  669. }
  670. providers := *response
  671. return providers.Providers, nil
  672. }
  673. // func (a *App) loadCustomKeybinds() {
  674. //
  675. // }