app.go 24 KB

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