app.go 24 KB

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