main.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. package main
  2. import (
  3. "context"
  4. "io"
  5. "log/slog"
  6. "os"
  7. "os/signal"
  8. "strings"
  9. "syscall"
  10. tea "github.com/charmbracelet/bubbletea/v2"
  11. flag "github.com/spf13/pflag"
  12. "github.com/sst/opencode-sdk-go"
  13. "github.com/sst/opencode-sdk-go/option"
  14. "github.com/sst/opencode/internal/api"
  15. "github.com/sst/opencode/internal/app"
  16. "github.com/sst/opencode/internal/clipboard"
  17. "github.com/sst/opencode/internal/tui"
  18. "github.com/sst/opencode/internal/util"
  19. "golang.org/x/sync/errgroup"
  20. )
  21. var Version = "dev"
  22. func main() {
  23. version := Version
  24. if version != "dev" && !strings.HasPrefix(Version, "v") {
  25. version = "v" + Version
  26. }
  27. var model *string = flag.String("model", "", "model to begin with")
  28. var prompt *string = flag.String("prompt", "", "prompt to begin with")
  29. var agent *string = flag.String("agent", "", "agent to begin with")
  30. var sessionID *string = flag.String("session", "", "session ID")
  31. flag.Parse()
  32. url := os.Getenv("OPENCODE_SERVER")
  33. stat, err := os.Stdin.Stat()
  34. if err != nil {
  35. slog.Error("Failed to stat stdin", "error", err)
  36. os.Exit(1)
  37. }
  38. // Check if there's data piped to stdin
  39. if (stat.Mode() & os.ModeCharDevice) == 0 {
  40. stdin, err := io.ReadAll(os.Stdin)
  41. if err != nil {
  42. slog.Error("Failed to read stdin", "error", err)
  43. os.Exit(1)
  44. }
  45. stdinContent := strings.TrimSpace(string(stdin))
  46. if stdinContent != "" {
  47. if prompt == nil || *prompt == "" {
  48. prompt = &stdinContent
  49. } else {
  50. combined := *prompt + "\n" + stdinContent
  51. prompt = &combined
  52. }
  53. }
  54. }
  55. httpClient := opencode.NewClient(
  56. option.WithBaseURL(url),
  57. )
  58. var agents []opencode.Agent
  59. var path *opencode.Path
  60. var project *opencode.Project
  61. batch := errgroup.Group{}
  62. batch.Go(func() error {
  63. result, err := httpClient.Project.Current(context.Background(), opencode.ProjectCurrentParams{})
  64. if err != nil {
  65. return err
  66. }
  67. project = result
  68. return nil
  69. })
  70. batch.Go(func() error {
  71. result, err := httpClient.Agent.List(context.Background(), opencode.AgentListParams{})
  72. if err != nil {
  73. return err
  74. }
  75. agents = *result
  76. return nil
  77. })
  78. batch.Go(func() error {
  79. result, err := httpClient.Path.Get(context.Background(), opencode.PathGetParams{})
  80. if err != nil {
  81. return err
  82. }
  83. path = result
  84. return nil
  85. })
  86. err = batch.Wait()
  87. if err != nil {
  88. panic(err)
  89. }
  90. ctx, cancel := context.WithCancel(context.Background())
  91. defer cancel()
  92. apiHandler := util.NewAPILogHandler(ctx, httpClient, "tui", slog.LevelDebug)
  93. logger := slog.New(apiHandler)
  94. slog.SetDefault(logger)
  95. slog.Debug("TUI launched")
  96. go func() {
  97. err = clipboard.Init()
  98. if err != nil {
  99. slog.Error("Failed to initialize clipboard", "error", err)
  100. }
  101. }()
  102. // Create main context for the application
  103. app_, err := app.New(ctx, version, project, path, agents, httpClient, model, prompt, agent, sessionID)
  104. if err != nil {
  105. panic(err)
  106. }
  107. tuiModel := tui.NewModel(app_).(*tui.Model)
  108. program := tea.NewProgram(
  109. tuiModel,
  110. tea.WithAltScreen(),
  111. tea.WithMouseCellMotion(),
  112. )
  113. // Set up signal handling for graceful shutdown
  114. sigChan := make(chan os.Signal, 1)
  115. signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
  116. go func() {
  117. stream := httpClient.Event.ListStreaming(ctx, opencode.EventListParams{})
  118. for stream.Next() {
  119. evt := stream.Current().AsUnion()
  120. program.Send(evt)
  121. }
  122. if err := stream.Err(); err != nil {
  123. slog.Error("Error streaming events", "error", err)
  124. program.Send(err)
  125. }
  126. }()
  127. go api.Start(ctx, program, httpClient)
  128. // Handle signals in a separate goroutine
  129. go func() {
  130. sig := <-sigChan
  131. slog.Info("Received signal, shutting down gracefully", "signal", sig)
  132. tuiModel.Cleanup()
  133. program.Quit()
  134. }()
  135. // Run the TUI
  136. result, err := program.Run()
  137. if err != nil {
  138. slog.Error("TUI error", "error", err)
  139. }
  140. tuiModel.Cleanup()
  141. slog.Info("TUI exited", "result", result)
  142. }