app.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963
  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) updateModelForNewAgent() {
  229. singleModelEnv := os.Getenv("OPENCODE_AGENTS_SWITCH_SINGLE_MODEL")
  230. isSingleModel := singleModelEnv == "1" || singleModelEnv == "true"
  231. if isSingleModel {
  232. return
  233. }
  234. // Set up model for the new agent
  235. modelID := a.Agent().Model.ModelID
  236. providerID := a.Agent().Model.ProviderID
  237. if modelID == "" {
  238. if model, ok := a.State.AgentModel[a.Agent().Name]; ok {
  239. modelID = model.ModelID
  240. providerID = model.ProviderID
  241. }
  242. }
  243. if modelID != "" {
  244. for _, provider := range a.Providers {
  245. if provider.ID == providerID {
  246. a.Provider = &provider
  247. for _, model := range provider.Models {
  248. if model.ID == modelID {
  249. a.Model = &model
  250. break
  251. }
  252. }
  253. break
  254. }
  255. }
  256. }
  257. }
  258. func (a *App) cycleMode(forward bool) (*App, tea.Cmd) {
  259. if forward {
  260. a.AgentIndex++
  261. if a.AgentIndex >= len(a.Agents) {
  262. a.AgentIndex = 0
  263. }
  264. } else {
  265. a.AgentIndex--
  266. if a.AgentIndex < 0 {
  267. a.AgentIndex = len(a.Agents) - 1
  268. }
  269. }
  270. if a.Agent().Mode == "subagent" {
  271. return a.cycleMode(forward)
  272. }
  273. a.updateModelForNewAgent()
  274. a.State.Agent = a.Agent().Name
  275. a.State.UpdateAgentUsage(a.Agent().Name)
  276. return a, a.SaveState()
  277. }
  278. func (a *App) SwitchAgent() (*App, tea.Cmd) {
  279. return a.cycleMode(true)
  280. }
  281. func (a *App) SwitchAgentReverse() (*App, tea.Cmd) {
  282. return a.cycleMode(false)
  283. }
  284. func (a *App) cycleRecentModel(forward bool) (*App, tea.Cmd) {
  285. recentModels := a.State.RecentlyUsedModels
  286. if len(recentModels) > 5 {
  287. recentModels = recentModels[:5]
  288. }
  289. if len(recentModels) < 2 {
  290. return a, toast.NewInfoToast("Need at least 2 recent models to cycle")
  291. }
  292. nextIndex := 0
  293. prevIndex := 0
  294. for i, recentModel := range recentModels {
  295. if a.Provider != nil && a.Model != nil && recentModel.ProviderID == a.Provider.ID &&
  296. recentModel.ModelID == a.Model.ID {
  297. nextIndex = (i + 1) % len(recentModels)
  298. prevIndex = (i - 1 + len(recentModels)) % len(recentModels)
  299. break
  300. }
  301. }
  302. targetIndex := nextIndex
  303. if !forward {
  304. targetIndex = prevIndex
  305. }
  306. for range recentModels {
  307. currentRecentModel := recentModels[targetIndex%len(recentModels)]
  308. provider, model := findModelByProviderAndModelID(
  309. a.Providers,
  310. currentRecentModel.ProviderID,
  311. currentRecentModel.ModelID,
  312. )
  313. if provider != nil && model != nil {
  314. a.Provider, a.Model = provider, model
  315. a.State.AgentModel[a.Agent().Name] = AgentModel{
  316. ProviderID: provider.ID,
  317. ModelID: model.ID,
  318. }
  319. return a, tea.Sequence(
  320. a.SaveState(),
  321. toast.NewSuccessToast(
  322. fmt.Sprintf("Switched to %s (%s)", model.Name, provider.Name),
  323. ),
  324. )
  325. }
  326. recentModels = append(
  327. recentModels[:targetIndex%len(recentModels)],
  328. recentModels[targetIndex%len(recentModels)+1:]...)
  329. if len(recentModels) < 2 {
  330. a.State.RecentlyUsedModels = recentModels
  331. return a, tea.Sequence(
  332. a.SaveState(),
  333. toast.NewInfoToast("Not enough valid recent models to cycle"),
  334. )
  335. }
  336. }
  337. a.State.RecentlyUsedModels = recentModels
  338. return a, toast.NewErrorToast("Recent model not found")
  339. }
  340. func (a *App) CycleRecentModel() (*App, tea.Cmd) {
  341. return a.cycleRecentModel(true)
  342. }
  343. func (a *App) CycleRecentModelReverse() (*App, tea.Cmd) {
  344. return a.cycleRecentModel(false)
  345. }
  346. func (a *App) SwitchToAgent(agentName string) (*App, tea.Cmd) {
  347. // Find the agent index by name
  348. for i, agent := range a.Agents {
  349. if agent.Name == agentName {
  350. a.AgentIndex = i
  351. break
  352. }
  353. }
  354. a.updateModelForNewAgent()
  355. a.State.Agent = a.Agent().Name
  356. a.State.UpdateAgentUsage(agentName)
  357. return a, a.SaveState()
  358. }
  359. // findModelByFullID finds a model by its full ID in the format "provider/model"
  360. func findModelByFullID(
  361. providers []opencode.Provider,
  362. fullModelID string,
  363. ) (*opencode.Provider, *opencode.Model) {
  364. modelParts := strings.SplitN(fullModelID, "/", 2)
  365. if len(modelParts) < 2 {
  366. return nil, nil
  367. }
  368. providerID := modelParts[0]
  369. modelID := modelParts[1]
  370. return findModelByProviderAndModelID(providers, providerID, modelID)
  371. }
  372. // findModelByProviderAndModelID finds a model by provider ID and model ID
  373. func findModelByProviderAndModelID(
  374. providers []opencode.Provider,
  375. providerID, modelID string,
  376. ) (*opencode.Provider, *opencode.Model) {
  377. for _, provider := range providers {
  378. if provider.ID != providerID {
  379. continue
  380. }
  381. for _, model := range provider.Models {
  382. if model.ID == modelID {
  383. return &provider, &model
  384. }
  385. }
  386. // Provider found but model not found
  387. return nil, nil
  388. }
  389. // Provider not found
  390. return nil, nil
  391. }
  392. // findProviderByID finds a provider by its ID
  393. func findProviderByID(providers []opencode.Provider, providerID string) *opencode.Provider {
  394. for _, provider := range providers {
  395. if provider.ID == providerID {
  396. return &provider
  397. }
  398. }
  399. return nil
  400. }
  401. func (a *App) InitializeProvider() tea.Cmd {
  402. providersResponse, err := a.Client.App.Providers(context.Background(), opencode.AppProvidersParams{})
  403. if err != nil {
  404. slog.Error("Failed to list providers", "error", err)
  405. // TODO: notify user
  406. return nil
  407. }
  408. providers := providersResponse.Providers
  409. if len(providers) == 0 {
  410. slog.Error("No providers configured")
  411. return nil
  412. }
  413. a.Providers = providers
  414. // retains backwards compatibility with old state format
  415. if model, ok := a.State.AgentModel[a.State.Agent]; ok {
  416. a.State.Provider = model.ProviderID
  417. a.State.Model = model.ModelID
  418. }
  419. var selectedProvider *opencode.Provider
  420. var selectedModel *opencode.Model
  421. // Priority 1: Command line --model flag (InitialModel)
  422. if a.InitialModel != nil && *a.InitialModel != "" {
  423. if provider, model := findModelByFullID(providers, *a.InitialModel); provider != nil &&
  424. model != nil {
  425. selectedProvider = provider
  426. selectedModel = model
  427. slog.Debug(
  428. "Selected model from command line",
  429. "provider",
  430. provider.ID,
  431. "model",
  432. model.ID,
  433. )
  434. } else {
  435. slog.Debug("Command line model not found", "model", *a.InitialModel)
  436. }
  437. }
  438. // Priority 2: Current agent's preferred model
  439. if selectedProvider == nil && a.Agent().Model.ModelID != "" {
  440. if provider, model := findModelByProviderAndModelID(providers, a.Agent().Model.ProviderID, a.Agent().Model.ModelID); provider != nil &&
  441. model != nil {
  442. selectedProvider = provider
  443. selectedModel = model
  444. slog.Debug(
  445. "Selected model from current agent",
  446. "provider",
  447. provider.ID,
  448. "model",
  449. model.ID,
  450. "agent",
  451. a.Agent().Name,
  452. )
  453. } else {
  454. slog.Debug("Agent model not found", "provider", a.Agent().Model.ProviderID, "model", a.Agent().Model.ModelID, "agent", a.Agent().Name)
  455. }
  456. }
  457. // Priority 3: Config file model setting
  458. if selectedProvider == nil && a.Config.Model != "" {
  459. if provider, model := findModelByFullID(providers, a.Config.Model); provider != nil &&
  460. model != nil {
  461. selectedProvider = provider
  462. selectedModel = model
  463. slog.Debug("Selected model from config", "provider", provider.ID, "model", model.ID)
  464. } else {
  465. slog.Debug("Config model not found", "model", a.Config.Model)
  466. }
  467. }
  468. // Priority 4: Recent model usage (most recently used model)
  469. if selectedProvider == nil && len(a.State.RecentlyUsedModels) > 0 {
  470. recentUsage := a.State.RecentlyUsedModels[0] // Most recent is first
  471. if provider, model := findModelByProviderAndModelID(providers, recentUsage.ProviderID, recentUsage.ModelID); provider != nil &&
  472. model != nil {
  473. selectedProvider = provider
  474. selectedModel = model
  475. slog.Debug(
  476. "Selected model from recent usage",
  477. "provider",
  478. provider.ID,
  479. "model",
  480. model.ID,
  481. )
  482. } else {
  483. slog.Debug("Recent model not found", "provider", recentUsage.ProviderID, "model", recentUsage.ModelID)
  484. }
  485. }
  486. // Priority 5: State-based model (backwards compatibility)
  487. if selectedProvider == nil && a.State.Provider != "" && a.State.Model != "" {
  488. if provider, model := findModelByProviderAndModelID(providers, a.State.Provider, a.State.Model); provider != nil &&
  489. model != nil {
  490. selectedProvider = provider
  491. selectedModel = model
  492. slog.Debug("Selected model from state", "provider", provider.ID, "model", model.ID)
  493. } else {
  494. slog.Debug("State model not found", "provider", a.State.Provider, "model", a.State.Model)
  495. }
  496. }
  497. // Priority 6: Internal priority fallback (Anthropic preferred, then first available)
  498. if selectedProvider == nil {
  499. // Try Anthropic first as internal priority
  500. if provider := findProviderByID(providers, "anthropic"); provider != nil {
  501. if model := getDefaultModel(providersResponse, *provider); model != nil {
  502. selectedProvider = provider
  503. selectedModel = model
  504. slog.Debug(
  505. "Selected model from internal priority (Anthropic)",
  506. "provider",
  507. provider.ID,
  508. "model",
  509. model.ID,
  510. )
  511. }
  512. }
  513. // If Anthropic not available, use first available provider
  514. if selectedProvider == nil && len(providers) > 0 {
  515. provider := &providers[0]
  516. if model := getDefaultModel(providersResponse, *provider); model != nil {
  517. selectedProvider = provider
  518. selectedModel = model
  519. slog.Debug(
  520. "Selected model from fallback (first available)",
  521. "provider",
  522. provider.ID,
  523. "model",
  524. model.ID,
  525. )
  526. }
  527. }
  528. }
  529. // Final safety check
  530. if selectedProvider == nil || selectedModel == nil {
  531. slog.Error("Failed to select any model")
  532. return nil
  533. }
  534. var cmds []tea.Cmd
  535. cmds = append(cmds, util.CmdHandler(ModelSelectedMsg{
  536. Provider: *selectedProvider,
  537. Model: *selectedModel,
  538. }))
  539. // Load initial session if provided
  540. if a.InitialSession != nil && *a.InitialSession != "" {
  541. cmds = append(cmds, func() tea.Msg {
  542. // Find the session by ID
  543. sessions, err := a.ListSessions(context.Background())
  544. if err != nil {
  545. slog.Error("Failed to list sessions for initial session", "error", err)
  546. return toast.NewErrorToast("Failed to load initial session")()
  547. }
  548. for _, session := range sessions {
  549. if session.ID == *a.InitialSession {
  550. return SessionSelectedMsg(&session)
  551. }
  552. }
  553. slog.Warn("Initial session not found", "sessionID", *a.InitialSession)
  554. return toast.NewErrorToast("Session not found: " + *a.InitialSession)()
  555. })
  556. }
  557. if a.InitialPrompt != nil && *a.InitialPrompt != "" {
  558. cmds = append(cmds, util.CmdHandler(SendPrompt{Text: *a.InitialPrompt}))
  559. }
  560. return tea.Sequence(cmds...)
  561. }
  562. func getDefaultModel(
  563. response *opencode.AppProvidersResponse,
  564. provider opencode.Provider,
  565. ) *opencode.Model {
  566. if match, ok := response.Default[provider.ID]; ok {
  567. model := provider.Models[match]
  568. return &model
  569. } else {
  570. for _, model := range provider.Models {
  571. return &model
  572. }
  573. }
  574. return nil
  575. }
  576. func (a *App) IsBusy() bool {
  577. if len(a.Messages) == 0 {
  578. return false
  579. }
  580. if a.IsCompacting() {
  581. return true
  582. }
  583. lastMessage := a.Messages[len(a.Messages)-1]
  584. if casted, ok := lastMessage.Info.(opencode.AssistantMessage); ok {
  585. return casted.Time.Completed == 0
  586. }
  587. return false
  588. }
  589. func (a *App) IsCompacting() bool {
  590. if time.Since(time.UnixMilli(int64(a.Session.Time.Compacting))) < time.Second*30 {
  591. return true
  592. }
  593. return false
  594. }
  595. func (a *App) HasAnimatingWork() bool {
  596. for _, msg := range a.Messages {
  597. switch casted := msg.Info.(type) {
  598. case opencode.AssistantMessage:
  599. if casted.Time.Completed == 0 {
  600. return true
  601. }
  602. }
  603. for _, p := range msg.Parts {
  604. if tp, ok := p.(opencode.ToolPart); ok {
  605. if tp.State.Status == opencode.ToolPartStateStatusPending {
  606. return true
  607. }
  608. }
  609. }
  610. }
  611. return false
  612. }
  613. func (a *App) SaveState() tea.Cmd {
  614. return func() tea.Msg {
  615. err := SaveState(a.StatePath, a.State)
  616. if err != nil {
  617. slog.Error("Failed to save state", "error", err)
  618. }
  619. return nil
  620. }
  621. }
  622. func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
  623. cmds := []tea.Cmd{}
  624. session, err := a.CreateSession(ctx)
  625. if err != nil {
  626. // status.Error(err.Error())
  627. return nil
  628. }
  629. a.Session = session
  630. cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
  631. go func() {
  632. _, err := a.Client.Session.Init(ctx, a.Session.ID, opencode.SessionInitParams{
  633. MessageID: opencode.F(id.Ascending(id.Message)),
  634. ProviderID: opencode.F(a.Provider.ID),
  635. ModelID: opencode.F(a.Model.ID),
  636. })
  637. if err != nil {
  638. slog.Error("Failed to initialize project", "error", err)
  639. // status.Error(err.Error())
  640. }
  641. }()
  642. return tea.Batch(cmds...)
  643. }
  644. func (a *App) CompactSession(ctx context.Context) tea.Cmd {
  645. if a.compactCancel != nil {
  646. a.compactCancel()
  647. }
  648. compactCtx, cancel := context.WithCancel(ctx)
  649. a.compactCancel = cancel
  650. go func() {
  651. defer func() {
  652. a.compactCancel = nil
  653. }()
  654. _, err := a.Client.Session.Summarize(
  655. compactCtx,
  656. a.Session.ID,
  657. opencode.SessionSummarizeParams{
  658. ProviderID: opencode.F(a.Provider.ID),
  659. ModelID: opencode.F(a.Model.ID),
  660. },
  661. )
  662. if err != nil {
  663. if compactCtx.Err() != context.Canceled {
  664. slog.Error("Failed to compact session", "error", err)
  665. }
  666. }
  667. }()
  668. return nil
  669. }
  670. func (a *App) MarkProjectInitialized(ctx context.Context) error {
  671. return nil
  672. /*
  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. }
  681. func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) {
  682. session, err := a.Client.Session.New(ctx, opencode.SessionNewParams{})
  683. if err != nil {
  684. return nil, err
  685. }
  686. return session, nil
  687. }
  688. func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) {
  689. var cmds []tea.Cmd
  690. if a.Session.ID == "" {
  691. session, err := a.CreateSession(ctx)
  692. if err != nil {
  693. return a, toast.NewErrorToast(err.Error())
  694. }
  695. a.Session = session
  696. cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
  697. }
  698. messageID := id.Ascending(id.Message)
  699. message := prompt.ToMessage(messageID, a.Session.ID)
  700. a.Messages = append(a.Messages, message)
  701. cmds = append(cmds, func() tea.Msg {
  702. _, err := a.Client.Session.Prompt(ctx, a.Session.ID, opencode.SessionPromptParams{
  703. Model: opencode.F(opencode.SessionPromptParamsModel{
  704. ProviderID: opencode.F(a.Provider.ID),
  705. ModelID: opencode.F(a.Model.ID),
  706. }),
  707. Agent: opencode.F(a.Agent().Name),
  708. MessageID: opencode.F(messageID),
  709. Parts: opencode.F(message.ToSessionChatParams()),
  710. })
  711. if err != nil {
  712. errormsg := fmt.Sprintf("failed to send message: %v", err)
  713. slog.Error(errormsg)
  714. return toast.NewErrorToast(errormsg)()
  715. }
  716. return nil
  717. })
  718. // The actual response will come through SSE
  719. // For now, just return success
  720. return a, tea.Batch(cmds...)
  721. }
  722. func (a *App) SendCommand(ctx context.Context, command string, args string) (*App, tea.Cmd) {
  723. var cmds []tea.Cmd
  724. if a.Session.ID == "" {
  725. session, err := a.CreateSession(ctx)
  726. if err != nil {
  727. return a, toast.NewErrorToast(err.Error())
  728. }
  729. a.Session = session
  730. cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
  731. }
  732. cmds = append(cmds, func() tea.Msg {
  733. params := opencode.SessionCommandParams{
  734. Command: opencode.F(command),
  735. Arguments: opencode.F(args),
  736. Agent: opencode.F(a.Agents[a.AgentIndex].Name),
  737. }
  738. if a.Provider != nil && a.Model != nil {
  739. params.Model = opencode.F(a.Provider.ID + "/" + a.Model.ID)
  740. }
  741. _, err := a.Client.Session.Command(
  742. context.Background(),
  743. a.Session.ID,
  744. params,
  745. )
  746. if err != nil {
  747. slog.Error("Failed to execute command", "error", err)
  748. return toast.NewErrorToast(fmt.Sprintf("Failed to execute command: %v", err))()
  749. }
  750. return nil
  751. })
  752. // The actual response will come through SSE
  753. // For now, just return success
  754. return a, tea.Batch(cmds...)
  755. }
  756. func (a *App) SendShell(ctx context.Context, command string) (*App, tea.Cmd) {
  757. var cmds []tea.Cmd
  758. if a.Session.ID == "" {
  759. session, err := a.CreateSession(ctx)
  760. if err != nil {
  761. return a, toast.NewErrorToast(err.Error())
  762. }
  763. a.Session = session
  764. cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
  765. }
  766. cmds = append(cmds, func() tea.Msg {
  767. _, err := a.Client.Session.Shell(
  768. context.Background(),
  769. a.Session.ID,
  770. opencode.SessionShellParams{
  771. Agent: opencode.F(a.Agent().Name),
  772. Command: opencode.F(command),
  773. },
  774. )
  775. if err != nil {
  776. slog.Error("Failed to submit shell command", "error", err)
  777. return toast.NewErrorToast(fmt.Sprintf("Failed to submit shell command: %v", err))()
  778. }
  779. return nil
  780. })
  781. // The actual response will come through SSE
  782. // For now, just return success
  783. return a, tea.Batch(cmds...)
  784. }
  785. func (a *App) Cancel(ctx context.Context, sessionID string) error {
  786. // Cancel any running compact operation
  787. if a.compactCancel != nil {
  788. a.compactCancel()
  789. a.compactCancel = nil
  790. }
  791. _, err := a.Client.Session.Abort(ctx, sessionID, opencode.SessionAbortParams{})
  792. if err != nil {
  793. slog.Error("Failed to cancel session", "error", err)
  794. return err
  795. }
  796. return nil
  797. }
  798. func (a *App) ListSessions(ctx context.Context) ([]opencode.Session, error) {
  799. response, err := a.Client.Session.List(ctx, opencode.SessionListParams{})
  800. if err != nil {
  801. return nil, err
  802. }
  803. if response == nil {
  804. return []opencode.Session{}, nil
  805. }
  806. sessions := *response
  807. return sessions, nil
  808. }
  809. func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
  810. _, err := a.Client.Session.Delete(ctx, sessionID, opencode.SessionDeleteParams{})
  811. if err != nil {
  812. slog.Error("Failed to delete session", "error", err)
  813. return err
  814. }
  815. return nil
  816. }
  817. func (a *App) UpdateSession(ctx context.Context, sessionID string, title string) error {
  818. _, err := a.Client.Session.Update(ctx, sessionID, opencode.SessionUpdateParams{
  819. Title: opencode.F(title),
  820. })
  821. if err != nil {
  822. slog.Error("Failed to update session", "error", err)
  823. return err
  824. }
  825. return nil
  826. }
  827. func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, error) {
  828. response, err := a.Client.Session.Messages(ctx, sessionId, opencode.SessionMessagesParams{})
  829. if err != nil {
  830. return nil, err
  831. }
  832. if response == nil {
  833. return []Message{}, nil
  834. }
  835. messages := []Message{}
  836. for _, message := range *response {
  837. msg := Message{
  838. Info: message.Info.AsUnion(),
  839. Parts: []opencode.PartUnion{},
  840. }
  841. for _, part := range message.Parts {
  842. msg.Parts = append(msg.Parts, part.AsUnion())
  843. }
  844. messages = append(messages, msg)
  845. }
  846. return messages, nil
  847. }
  848. func (a *App) ListProviders(ctx context.Context) ([]opencode.Provider, error) {
  849. response, err := a.Client.App.Providers(ctx, opencode.AppProvidersParams{})
  850. if err != nil {
  851. return nil, err
  852. }
  853. if response == nil {
  854. return []opencode.Provider{}, nil
  855. }
  856. providers := *response
  857. return providers.Providers, nil
  858. }
  859. // func (a *App) loadCustomKeybinds() {
  860. //
  861. // }