app.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955
  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. IsBashMode bool
  48. ScrollSpeed int
  49. }
  50. func (a *App) Agent() *opencode.Agent {
  51. return &a.Agents[a.AgentIndex]
  52. }
  53. type SessionCreatedMsg = struct {
  54. Session *opencode.Session
  55. }
  56. type SessionSelectedMsg = *opencode.Session
  57. type MessageRevertedMsg struct {
  58. Session opencode.Session
  59. Message Message
  60. }
  61. type SessionUnrevertedMsg struct {
  62. Session opencode.Session
  63. }
  64. type SessionLoadedMsg struct{}
  65. type ModelSelectedMsg struct {
  66. Provider opencode.Provider
  67. Model opencode.Model
  68. }
  69. type AgentSelectedMsg struct {
  70. AgentName string
  71. }
  72. type SessionClearedMsg struct{}
  73. type CompactSessionMsg struct{}
  74. type SendPrompt = Prompt
  75. type SendShell = struct {
  76. Command string
  77. }
  78. type SendCommand = struct {
  79. Command string
  80. Args string
  81. }
  82. type SetEditorContentMsg struct {
  83. Text string
  84. }
  85. type FileRenderedMsg struct {
  86. FilePath string
  87. }
  88. type PermissionRespondedToMsg struct {
  89. Response opencode.SessionPermissionRespondParamsResponse
  90. }
  91. func New(
  92. ctx context.Context,
  93. version string,
  94. appInfo opencode.App,
  95. agents []opencode.Agent,
  96. httpClient *opencode.Client,
  97. initialModel *string,
  98. initialPrompt *string,
  99. initialAgent *string,
  100. initialSession *string,
  101. ) (*App, error) {
  102. util.RootPath = appInfo.Path.Root
  103. util.CwdPath = appInfo.Path.Cwd
  104. configInfo, err := httpClient.Config.Get(ctx)
  105. if err != nil {
  106. return nil, err
  107. }
  108. if configInfo.Keybinds.Leader == "" {
  109. configInfo.Keybinds.Leader = "ctrl+x"
  110. }
  111. appStatePath := filepath.Join(appInfo.Path.State, "tui")
  112. appState, err := LoadState(appStatePath)
  113. if err != nil {
  114. appState = NewState()
  115. SaveState(appStatePath, appState)
  116. }
  117. if appState.AgentModel == nil {
  118. appState.AgentModel = make(map[string]AgentModel)
  119. }
  120. if configInfo.Theme != "" {
  121. appState.Theme = configInfo.Theme
  122. }
  123. themeEnv := os.Getenv("OPENCODE_THEME")
  124. if themeEnv != "" {
  125. appState.Theme = themeEnv
  126. }
  127. agentIndex := slices.IndexFunc(agents, func(a opencode.Agent) bool {
  128. return a.Mode != "subagent"
  129. })
  130. var agent *opencode.Agent
  131. modeName := "build"
  132. if appState.Agent != "" {
  133. modeName = appState.Agent
  134. }
  135. if initialAgent != nil && *initialAgent != "" {
  136. modeName = *initialAgent
  137. }
  138. for i, m := range agents {
  139. if m.Name == modeName {
  140. agentIndex = i
  141. break
  142. }
  143. }
  144. agent = &agents[agentIndex]
  145. if agent.Model.ModelID != "" {
  146. appState.AgentModel[agent.Name] = AgentModel{
  147. ProviderID: agent.Model.ProviderID,
  148. ModelID: agent.Model.ModelID,
  149. }
  150. }
  151. if err := theme.LoadThemesFromDirectories(
  152. appInfo.Path.Config,
  153. appInfo.Path.Root,
  154. appInfo.Path.Cwd,
  155. ); err != nil {
  156. slog.Warn("Failed to load themes from directories", "error", err)
  157. }
  158. if appState.Theme != "" {
  159. if appState.Theme == "system" && styles.Terminal != nil {
  160. theme.UpdateSystemTheme(
  161. styles.Terminal.Background,
  162. styles.Terminal.BackgroundIsDark,
  163. )
  164. }
  165. theme.SetTheme(appState.Theme)
  166. }
  167. slog.Debug("Loaded config", "config", configInfo)
  168. customCommands, err := httpClient.Command.List(ctx)
  169. if err != nil {
  170. return nil, err
  171. }
  172. app := &App{
  173. Info: appInfo,
  174. Agents: agents,
  175. Version: version,
  176. StatePath: appStatePath,
  177. Config: configInfo,
  178. State: appState,
  179. Client: httpClient,
  180. AgentIndex: agentIndex,
  181. Session: &opencode.Session{},
  182. Messages: []Message{},
  183. Commands: commands.LoadFromConfig(configInfo, *customCommands),
  184. InitialModel: initialModel,
  185. InitialPrompt: initialPrompt,
  186. InitialAgent: initialAgent,
  187. InitialSession: initialSession,
  188. ScrollSpeed: int(configInfo.Tui.ScrollSpeed),
  189. }
  190. return app, nil
  191. }
  192. func (a *App) Keybind(commandName commands.CommandName) string {
  193. command := a.Commands[commandName]
  194. if len(command.Keybindings) == 0 {
  195. return ""
  196. }
  197. kb := command.Keybindings[0]
  198. key := kb.Key
  199. if kb.RequiresLeader {
  200. key = a.Config.Keybinds.Leader + " " + kb.Key
  201. }
  202. return key
  203. }
  204. func (a *App) Key(commandName commands.CommandName) string {
  205. t := theme.CurrentTheme()
  206. base := styles.NewStyle().Background(t.Background()).Foreground(t.Text()).Bold(true).Render
  207. muted := styles.NewStyle().
  208. Background(t.Background()).
  209. Foreground(t.TextMuted()).
  210. Faint(true).
  211. Render
  212. command := a.Commands[commandName]
  213. key := a.Keybind(commandName)
  214. return base(key) + muted(" "+command.Description)
  215. }
  216. func SetClipboard(text string) tea.Cmd {
  217. var cmds []tea.Cmd
  218. cmds = append(cmds, func() tea.Msg {
  219. clipboard.Write(clipboard.FmtText, []byte(text))
  220. return nil
  221. })
  222. // try to set the clipboard using OSC52 for terminals that support it
  223. cmds = append(cmds, tea.SetClipboard(text))
  224. return tea.Sequence(cmds...)
  225. }
  226. func (a *App) cycleMode(forward bool) (*App, tea.Cmd) {
  227. if forward {
  228. a.AgentIndex++
  229. if a.AgentIndex >= len(a.Agents) {
  230. a.AgentIndex = 0
  231. }
  232. } else {
  233. a.AgentIndex--
  234. if a.AgentIndex < 0 {
  235. a.AgentIndex = len(a.Agents) - 1
  236. }
  237. }
  238. if a.Agent().Mode == "subagent" {
  239. return a.cycleMode(forward)
  240. }
  241. modelID := a.Agent().Model.ModelID
  242. providerID := a.Agent().Model.ProviderID
  243. if modelID == "" {
  244. if model, ok := a.State.AgentModel[a.Agent().Name]; ok {
  245. modelID = model.ModelID
  246. providerID = model.ProviderID
  247. }
  248. }
  249. if modelID != "" {
  250. for _, provider := range a.Providers {
  251. if provider.ID == providerID {
  252. a.Provider = &provider
  253. for _, model := range provider.Models {
  254. if model.ID == modelID {
  255. a.Model = &model
  256. break
  257. }
  258. }
  259. break
  260. }
  261. }
  262. }
  263. a.State.Agent = a.Agent().Name
  264. a.State.UpdateAgentUsage(a.Agent().Name)
  265. return a, a.SaveState()
  266. }
  267. func (a *App) SwitchAgent() (*App, tea.Cmd) {
  268. return a.cycleMode(true)
  269. }
  270. func (a *App) SwitchAgentReverse() (*App, tea.Cmd) {
  271. return a.cycleMode(false)
  272. }
  273. func (a *App) cycleRecentModel(forward bool) (*App, tea.Cmd) {
  274. recentModels := a.State.RecentlyUsedModels
  275. if len(recentModels) > 5 {
  276. recentModels = recentModels[:5]
  277. }
  278. if len(recentModels) < 2 {
  279. return a, toast.NewInfoToast("Need at least 2 recent models to cycle")
  280. }
  281. nextIndex := 0
  282. prevIndex := 0
  283. for i, recentModel := range recentModels {
  284. if a.Provider != nil && a.Model != nil && recentModel.ProviderID == a.Provider.ID &&
  285. recentModel.ModelID == a.Model.ID {
  286. nextIndex = (i + 1) % len(recentModels)
  287. prevIndex = (i - 1 + len(recentModels)) % len(recentModels)
  288. break
  289. }
  290. }
  291. targetIndex := nextIndex
  292. if !forward {
  293. targetIndex = prevIndex
  294. }
  295. for range recentModels {
  296. currentRecentModel := recentModels[targetIndex%len(recentModels)]
  297. provider, model := findModelByProviderAndModelID(
  298. a.Providers,
  299. currentRecentModel.ProviderID,
  300. currentRecentModel.ModelID,
  301. )
  302. if provider != nil && model != nil {
  303. a.Provider, a.Model = provider, model
  304. a.State.AgentModel[a.Agent().Name] = AgentModel{
  305. ProviderID: provider.ID,
  306. ModelID: model.ID,
  307. }
  308. return a, tea.Sequence(
  309. a.SaveState(),
  310. toast.NewSuccessToast(
  311. fmt.Sprintf("Switched to %s (%s)", model.Name, provider.Name),
  312. ),
  313. )
  314. }
  315. recentModels = append(
  316. recentModels[:targetIndex%len(recentModels)],
  317. recentModels[targetIndex%len(recentModels)+1:]...)
  318. if len(recentModels) < 2 {
  319. a.State.RecentlyUsedModels = recentModels
  320. return a, tea.Sequence(
  321. a.SaveState(),
  322. toast.NewInfoToast("Not enough valid recent models to cycle"),
  323. )
  324. }
  325. }
  326. a.State.RecentlyUsedModels = recentModels
  327. return a, toast.NewErrorToast("Recent model not found")
  328. }
  329. func (a *App) CycleRecentModel() (*App, tea.Cmd) {
  330. return a.cycleRecentModel(true)
  331. }
  332. func (a *App) CycleRecentModelReverse() (*App, tea.Cmd) {
  333. return a.cycleRecentModel(false)
  334. }
  335. func (a *App) SwitchToAgent(agentName string) (*App, tea.Cmd) {
  336. // Find the agent index by name
  337. for i, agent := range a.Agents {
  338. if agent.Name == agentName {
  339. a.AgentIndex = i
  340. break
  341. }
  342. }
  343. // Set up model for the new agent
  344. modelID := a.Agent().Model.ModelID
  345. providerID := a.Agent().Model.ProviderID
  346. if modelID == "" {
  347. if model, ok := a.State.AgentModel[a.Agent().Name]; ok {
  348. modelID = model.ModelID
  349. providerID = model.ProviderID
  350. }
  351. }
  352. if modelID != "" {
  353. for _, provider := range a.Providers {
  354. if provider.ID == providerID {
  355. a.Provider = &provider
  356. for _, model := range provider.Models {
  357. if model.ID == modelID {
  358. a.Model = &model
  359. break
  360. }
  361. }
  362. break
  363. }
  364. }
  365. }
  366. a.State.Agent = a.Agent().Name
  367. a.State.UpdateAgentUsage(agentName)
  368. return a, a.SaveState()
  369. }
  370. // findModelByFullID finds a model by its full ID in the format "provider/model"
  371. func findModelByFullID(
  372. providers []opencode.Provider,
  373. fullModelID string,
  374. ) (*opencode.Provider, *opencode.Model) {
  375. modelParts := strings.SplitN(fullModelID, "/", 2)
  376. if len(modelParts) < 2 {
  377. return nil, nil
  378. }
  379. providerID := modelParts[0]
  380. modelID := modelParts[1]
  381. return findModelByProviderAndModelID(providers, providerID, modelID)
  382. }
  383. // findModelByProviderAndModelID finds a model by provider ID and model ID
  384. func findModelByProviderAndModelID(
  385. providers []opencode.Provider,
  386. providerID, modelID string,
  387. ) (*opencode.Provider, *opencode.Model) {
  388. for _, provider := range providers {
  389. if provider.ID != providerID {
  390. continue
  391. }
  392. for _, model := range provider.Models {
  393. if model.ID == modelID {
  394. return &provider, &model
  395. }
  396. }
  397. // Provider found but model not found
  398. return nil, nil
  399. }
  400. // Provider not found
  401. return nil, nil
  402. }
  403. // findProviderByID finds a provider by its ID
  404. func findProviderByID(providers []opencode.Provider, providerID string) *opencode.Provider {
  405. for _, provider := range providers {
  406. if provider.ID == providerID {
  407. return &provider
  408. }
  409. }
  410. return nil
  411. }
  412. func (a *App) InitializeProvider() tea.Cmd {
  413. providersResponse, err := a.Client.App.Providers(context.Background())
  414. if err != nil {
  415. slog.Error("Failed to list providers", "error", err)
  416. // TODO: notify user
  417. return nil
  418. }
  419. providers := providersResponse.Providers
  420. if len(providers) == 0 {
  421. slog.Error("No providers configured")
  422. return nil
  423. }
  424. a.Providers = providers
  425. // retains backwards compatibility with old state format
  426. if model, ok := a.State.AgentModel[a.State.Agent]; ok {
  427. a.State.Provider = model.ProviderID
  428. a.State.Model = model.ModelID
  429. }
  430. var selectedProvider *opencode.Provider
  431. var selectedModel *opencode.Model
  432. // Priority 1: Command line --model flag (InitialModel)
  433. if a.InitialModel != nil && *a.InitialModel != "" {
  434. if provider, model := findModelByFullID(providers, *a.InitialModel); provider != nil &&
  435. model != nil {
  436. selectedProvider = provider
  437. selectedModel = model
  438. slog.Debug(
  439. "Selected model from command line",
  440. "provider",
  441. provider.ID,
  442. "model",
  443. model.ID,
  444. )
  445. } else {
  446. slog.Debug("Command line model not found", "model", *a.InitialModel)
  447. }
  448. }
  449. // Priority 2: Config file model setting
  450. if selectedProvider == nil && a.Config.Model != "" {
  451. if provider, model := findModelByFullID(providers, a.Config.Model); provider != nil &&
  452. model != nil {
  453. selectedProvider = provider
  454. selectedModel = model
  455. slog.Debug("Selected model from config", "provider", provider.ID, "model", model.ID)
  456. } else {
  457. slog.Debug("Config model not found", "model", a.Config.Model)
  458. }
  459. }
  460. // Priority 3: Current agent's preferred model
  461. if selectedProvider == nil && a.Agent().Model.ModelID != "" {
  462. if provider, model := findModelByProviderAndModelID(providers, a.Agent().Model.ProviderID, a.Agent().Model.ModelID); provider != nil &&
  463. model != nil {
  464. selectedProvider = provider
  465. selectedModel = model
  466. slog.Debug(
  467. "Selected model from current agent",
  468. "provider",
  469. provider.ID,
  470. "model",
  471. model.ID,
  472. "agent",
  473. a.Agent().Name,
  474. )
  475. } else {
  476. slog.Debug("Agent model not found", "provider", a.Agent().Model.ProviderID, "model", a.Agent().Model.ModelID, "agent", a.Agent().Name)
  477. }
  478. }
  479. // Priority 4: Recent model usage (most recently used model)
  480. if selectedProvider == nil && len(a.State.RecentlyUsedModels) > 0 {
  481. recentUsage := a.State.RecentlyUsedModels[0] // Most recent is first
  482. if provider, model := findModelByProviderAndModelID(providers, recentUsage.ProviderID, recentUsage.ModelID); provider != nil &&
  483. model != nil {
  484. selectedProvider = provider
  485. selectedModel = model
  486. slog.Debug(
  487. "Selected model from recent usage",
  488. "provider",
  489. provider.ID,
  490. "model",
  491. model.ID,
  492. )
  493. } else {
  494. slog.Debug("Recent model not found", "provider", recentUsage.ProviderID, "model", recentUsage.ModelID)
  495. }
  496. }
  497. // Priority 5: State-based model (backwards compatibility)
  498. if selectedProvider == nil && a.State.Provider != "" && a.State.Model != "" {
  499. if provider, model := findModelByProviderAndModelID(providers, a.State.Provider, a.State.Model); provider != nil &&
  500. model != nil {
  501. selectedProvider = provider
  502. selectedModel = model
  503. slog.Debug("Selected model from state", "provider", provider.ID, "model", model.ID)
  504. } else {
  505. slog.Debug("State model not found", "provider", a.State.Provider, "model", a.State.Model)
  506. }
  507. }
  508. // Priority 6: Internal priority fallback (Anthropic preferred, then first available)
  509. if selectedProvider == nil {
  510. // Try Anthropic first as internal priority
  511. if provider := findProviderByID(providers, "anthropic"); provider != nil {
  512. if model := getDefaultModel(providersResponse, *provider); model != nil {
  513. selectedProvider = provider
  514. selectedModel = model
  515. slog.Debug(
  516. "Selected model from internal priority (Anthropic)",
  517. "provider",
  518. provider.ID,
  519. "model",
  520. model.ID,
  521. )
  522. }
  523. }
  524. // If Anthropic not available, use first available provider
  525. if selectedProvider == nil && len(providers) > 0 {
  526. provider := &providers[0]
  527. if model := getDefaultModel(providersResponse, *provider); model != nil {
  528. selectedProvider = provider
  529. selectedModel = model
  530. slog.Debug(
  531. "Selected model from fallback (first available)",
  532. "provider",
  533. provider.ID,
  534. "model",
  535. model.ID,
  536. )
  537. }
  538. }
  539. }
  540. // Final safety check
  541. if selectedProvider == nil || selectedModel == nil {
  542. slog.Error("Failed to select any model")
  543. return nil
  544. }
  545. var cmds []tea.Cmd
  546. cmds = append(cmds, util.CmdHandler(ModelSelectedMsg{
  547. Provider: *selectedProvider,
  548. Model: *selectedModel,
  549. }))
  550. // Load initial session if provided
  551. if a.InitialSession != nil && *a.InitialSession != "" {
  552. cmds = append(cmds, func() tea.Msg {
  553. // Find the session by ID
  554. sessions, err := a.ListSessions(context.Background())
  555. if err != nil {
  556. slog.Error("Failed to list sessions for initial session", "error", err)
  557. return toast.NewErrorToast("Failed to load initial session")()
  558. }
  559. for _, session := range sessions {
  560. if session.ID == *a.InitialSession {
  561. return SessionSelectedMsg(&session)
  562. }
  563. }
  564. slog.Warn("Initial session not found", "sessionID", *a.InitialSession)
  565. return toast.NewErrorToast("Session not found: " + *a.InitialSession)()
  566. })
  567. }
  568. if a.InitialPrompt != nil && *a.InitialPrompt != "" {
  569. cmds = append(cmds, util.CmdHandler(SendPrompt{Text: *a.InitialPrompt}))
  570. }
  571. return tea.Sequence(cmds...)
  572. }
  573. func getDefaultModel(
  574. response *opencode.AppProvidersResponse,
  575. provider opencode.Provider,
  576. ) *opencode.Model {
  577. if match, ok := response.Default[provider.ID]; ok {
  578. model := provider.Models[match]
  579. return &model
  580. } else {
  581. for _, model := range provider.Models {
  582. return &model
  583. }
  584. }
  585. return nil
  586. }
  587. func (a *App) IsBusy() bool {
  588. if len(a.Messages) == 0 {
  589. return false
  590. }
  591. lastMessage := a.Messages[len(a.Messages)-1]
  592. if casted, ok := lastMessage.Info.(opencode.AssistantMessage); ok {
  593. return casted.Time.Completed == 0
  594. }
  595. return false
  596. }
  597. func (a *App) HasAnimatingWork() bool {
  598. for _, msg := range a.Messages {
  599. switch casted := msg.Info.(type) {
  600. case opencode.AssistantMessage:
  601. if casted.Time.Completed == 0 {
  602. return true
  603. }
  604. }
  605. for _, p := range msg.Parts {
  606. if tp, ok := p.(opencode.ToolPart); ok {
  607. if tp.State.Status == opencode.ToolPartStateStatusPending {
  608. return true
  609. }
  610. }
  611. }
  612. }
  613. return false
  614. }
  615. func (a *App) SaveState() tea.Cmd {
  616. return func() tea.Msg {
  617. err := SaveState(a.StatePath, a.State)
  618. if err != nil {
  619. slog.Error("Failed to save state", "error", err)
  620. }
  621. return nil
  622. }
  623. }
  624. func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
  625. cmds := []tea.Cmd{}
  626. session, err := a.CreateSession(ctx)
  627. if err != nil {
  628. // status.Error(err.Error())
  629. return nil
  630. }
  631. a.Session = session
  632. cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
  633. go func() {
  634. _, err := a.Client.Session.Init(ctx, a.Session.ID, opencode.SessionInitParams{
  635. MessageID: opencode.F(id.Ascending(id.Message)),
  636. ProviderID: opencode.F(a.Provider.ID),
  637. ModelID: opencode.F(a.Model.ID),
  638. })
  639. if err != nil {
  640. slog.Error("Failed to initialize project", "error", err)
  641. // status.Error(err.Error())
  642. }
  643. }()
  644. return tea.Batch(cmds...)
  645. }
  646. func (a *App) CompactSession(ctx context.Context) tea.Cmd {
  647. if a.compactCancel != nil {
  648. a.compactCancel()
  649. }
  650. compactCtx, cancel := context.WithCancel(ctx)
  651. a.compactCancel = cancel
  652. go func() {
  653. defer func() {
  654. a.compactCancel = nil
  655. }()
  656. _, err := a.Client.Session.Summarize(
  657. compactCtx,
  658. a.Session.ID,
  659. opencode.SessionSummarizeParams{
  660. ProviderID: opencode.F(a.Provider.ID),
  661. ModelID: opencode.F(a.Model.ID),
  662. },
  663. )
  664. if err != nil {
  665. if compactCtx.Err() != context.Canceled {
  666. slog.Error("Failed to compact session", "error", err)
  667. }
  668. }
  669. }()
  670. return nil
  671. }
  672. func (a *App) MarkProjectInitialized(ctx context.Context) error {
  673. _, err := a.Client.App.Init(ctx)
  674. if err != nil {
  675. slog.Error("Failed to mark project as initialized", "error", err)
  676. return err
  677. }
  678. return nil
  679. }
  680. func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) {
  681. session, err := a.Client.Session.New(ctx, opencode.SessionNewParams{})
  682. if err != nil {
  683. return nil, err
  684. }
  685. return session, nil
  686. }
  687. func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) {
  688. var cmds []tea.Cmd
  689. if a.Session.ID == "" {
  690. session, err := a.CreateSession(ctx)
  691. if err != nil {
  692. return a, toast.NewErrorToast(err.Error())
  693. }
  694. a.Session = session
  695. cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
  696. }
  697. messageID := id.Ascending(id.Message)
  698. message := prompt.ToMessage(messageID, a.Session.ID)
  699. a.Messages = append(a.Messages, message)
  700. cmds = append(cmds, func() tea.Msg {
  701. _, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{
  702. ProviderID: opencode.F(a.Provider.ID),
  703. ModelID: opencode.F(a.Model.ID),
  704. Agent: opencode.F(a.Agent().Name),
  705. MessageID: opencode.F(messageID),
  706. Parts: opencode.F(message.ToSessionChatParams()),
  707. })
  708. if err != nil {
  709. errormsg := fmt.Sprintf("failed to send message: %v", err)
  710. slog.Error(errormsg)
  711. return toast.NewErrorToast(errormsg)()
  712. }
  713. return nil
  714. })
  715. // The actual response will come through SSE
  716. // For now, just return success
  717. return a, tea.Batch(cmds...)
  718. }
  719. func (a *App) SendCommand(ctx context.Context, command string, args string) (*App, tea.Cmd) {
  720. var cmds []tea.Cmd
  721. if a.Session.ID == "" {
  722. session, err := a.CreateSession(ctx)
  723. if err != nil {
  724. return a, toast.NewErrorToast(err.Error())
  725. }
  726. a.Session = session
  727. cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
  728. }
  729. cmds = append(cmds, func() tea.Msg {
  730. _, err := a.Client.Session.Command(
  731. context.Background(),
  732. a.Session.ID,
  733. opencode.SessionCommandParams{
  734. Command: opencode.F(command),
  735. Arguments: opencode.F(args),
  736. Agent: opencode.F(a.Agents[a.AgentIndex].Name),
  737. Model: opencode.F(a.State.Provider + "/" + a.State.Model),
  738. },
  739. )
  740. if err != nil {
  741. slog.Error("Failed to execute command", "error", err)
  742. return toast.NewErrorToast("Failed to execute command")
  743. }
  744. return nil
  745. })
  746. // The actual response will come through SSE
  747. // For now, just return success
  748. return a, tea.Batch(cmds...)
  749. }
  750. func (a *App) SendShell(ctx context.Context, command string) (*App, tea.Cmd) {
  751. var cmds []tea.Cmd
  752. if a.Session.ID == "" {
  753. session, err := a.CreateSession(ctx)
  754. if err != nil {
  755. return a, toast.NewErrorToast(err.Error())
  756. }
  757. a.Session = session
  758. cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
  759. }
  760. cmds = append(cmds, func() tea.Msg {
  761. _, err := a.Client.Session.Shell(
  762. context.Background(),
  763. a.Session.ID,
  764. opencode.SessionShellParams{
  765. Agent: opencode.F(a.Agent().Name),
  766. Command: opencode.F(command),
  767. },
  768. )
  769. if err != nil {
  770. slog.Error("Failed to submit shell command", "error", err)
  771. return toast.NewErrorToast("Failed to submit shell command")()
  772. }
  773. return nil
  774. })
  775. // The actual response will come through SSE
  776. // For now, just return success
  777. return a, tea.Batch(cmds...)
  778. }
  779. func (a *App) Cancel(ctx context.Context, sessionID string) error {
  780. // Cancel any running compact operation
  781. if a.compactCancel != nil {
  782. a.compactCancel()
  783. a.compactCancel = nil
  784. }
  785. _, err := a.Client.Session.Abort(ctx, sessionID)
  786. if err != nil {
  787. slog.Error("Failed to cancel session", "error", err)
  788. return err
  789. }
  790. return nil
  791. }
  792. func (a *App) ListSessions(ctx context.Context) ([]opencode.Session, error) {
  793. response, err := a.Client.Session.List(ctx)
  794. if err != nil {
  795. return nil, err
  796. }
  797. if response == nil {
  798. return []opencode.Session{}, nil
  799. }
  800. sessions := *response
  801. return sessions, nil
  802. }
  803. func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
  804. _, err := a.Client.Session.Delete(ctx, sessionID)
  805. if err != nil {
  806. slog.Error("Failed to delete session", "error", err)
  807. return err
  808. }
  809. return nil
  810. }
  811. func (a *App) UpdateSession(ctx context.Context, sessionID string, title string) error {
  812. _, err := a.Client.Session.Update(ctx, sessionID, opencode.SessionUpdateParams{
  813. Title: opencode.F(title),
  814. })
  815. if err != nil {
  816. slog.Error("Failed to update session", "error", err)
  817. return err
  818. }
  819. return nil
  820. }
  821. func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, error) {
  822. response, err := a.Client.Session.Messages(ctx, sessionId)
  823. if err != nil {
  824. return nil, err
  825. }
  826. if response == nil {
  827. return []Message{}, nil
  828. }
  829. messages := []Message{}
  830. for _, message := range *response {
  831. msg := Message{
  832. Info: message.Info.AsUnion(),
  833. Parts: []opencode.PartUnion{},
  834. }
  835. for _, part := range message.Parts {
  836. msg.Parts = append(msg.Parts, part.AsUnion())
  837. }
  838. messages = append(messages, msg)
  839. }
  840. return messages, nil
  841. }
  842. func (a *App) ListProviders(ctx context.Context) ([]opencode.Provider, error) {
  843. response, err := a.Client.App.Providers(ctx)
  844. if err != nil {
  845. return nil, err
  846. }
  847. if response == nil {
  848. return []opencode.Provider{}, nil
  849. }
  850. providers := *response
  851. return providers.Providers, nil
  852. }
  853. // func (a *App) loadCustomKeybinds() {
  854. //
  855. // }