Skip to content

Commit 32131a4

Browse files
radutopalaRadu Topala
authored and
Radu Topala
committed
feat: non-interactive mode
1 parent 98e2910 commit 32131a4

File tree

5 files changed

+106
-14
lines changed

5 files changed

+106
-14
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ Thumbs.db
4343

4444
.opencode/
4545

46+
opencode

cmd/root.go

+37-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
tea "github.com/charmbracelet/bubbletea"
11+
zone "github.com/lrstanley/bubblezone"
1112
"github.com/opencode-ai/opencode/internal/app"
1213
"github.com/opencode-ai/opencode/internal/config"
1314
"github.com/opencode-ai/opencode/internal/db"
@@ -16,16 +17,32 @@ import (
1617
"github.com/opencode-ai/opencode/internal/pubsub"
1718
"github.com/opencode-ai/opencode/internal/tui"
1819
"github.com/opencode-ai/opencode/internal/version"
19-
zone "github.com/lrstanley/bubblezone"
2020
"github.com/spf13/cobra"
2121
)
2222

2323
var rootCmd = &cobra.Command{
24-
Use: "OpenCode",
25-
Short: "A terminal AI assistant for software development",
24+
Use: "opencode [prompt]",
25+
Args: cobra.MaximumNArgs(1),
26+
Short: "Terminal-based AI assistant for software development",
2627
Long: `OpenCode is a powerful terminal-based AI assistant that helps with software development tasks.
2728
It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
2829
to assist developers in writing, debugging, and understanding code directly from the terminal.`,
30+
Example: `
31+
# Run in interactive mode
32+
opencode
33+
34+
# Run with debug logging
35+
opencode -d
36+
37+
# Run with debug logging in a specific directory
38+
opencode -d -c /path/to/project
39+
40+
# Print version
41+
opencode -v
42+
43+
# Run a single non-interactive prompt
44+
opencode "Explain the use of context in Go"
45+
`,
2946
RunE: func(cmd *cobra.Command, args []string) error {
3047
// If the help flag is set, show the help message
3148
if cmd.Flag("help").Changed {
@@ -40,6 +57,11 @@ to assist developers in writing, debugging, and understanding code directly from
4057
// Load the config
4158
debug, _ := cmd.Flags().GetBool("debug")
4259
cwd, _ := cmd.Flags().GetString("cwd")
60+
var prompt string
61+
if len(args) == 1 {
62+
prompt = args[0]
63+
}
64+
4365
if cwd != "" {
4466
err := os.Chdir(cwd)
4567
if err != nil {
@@ -73,7 +95,19 @@ to assist developers in writing, debugging, and understanding code directly from
7395
logging.Error("Failed to create app: %v", err)
7496
return err
7597
}
98+
// Defer shutdown here so it runs for both interactive and non-interactive modes
99+
defer app.Shutdown()
100+
101+
// Initialize MCP tools early for both modes
102+
initMCPTools(ctx, app)
76103

104+
// Non-interactive mode
105+
if prompt != "" {
106+
// Run non-interactive flow using the App method
107+
return app.RunNonInteractive(ctx, prompt)
108+
}
109+
110+
// Interactive mode
77111
// Set up the TUI
78112
zone.NewGlobal()
79113
program := tea.NewProgram(
@@ -82,9 +116,6 @@ to assist developers in writing, debugging, and understanding code directly from
82116
tea.WithMouseCellMotion(),
83117
)
84118

85-
// Initialize MCP tools in the background
86-
initMCPTools(ctx, app)
87-
88119
// Setup the subscriptions, this will send services events to the TUI
89120
ch, cancelSubs := setupSubscriptions(app, ctx)
90121

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ require (
1313
github.com/aymanbagabas/go-udiff v0.2.0
1414
github.com/bmatcuk/doublestar/v4 v4.8.1
1515
github.com/catppuccin/go v0.3.0
16-
github.com/charmbracelet/bubbles v0.20.0
17-
github.com/charmbracelet/bubbletea v1.3.4
16+
github.com/charmbracelet/bubbles v0.21.0
17+
github.com/charmbracelet/bubbletea v1.3.5
1818
github.com/charmbracelet/glamour v0.9.1
1919
github.com/charmbracelet/huh v0.6.0
2020
github.com/charmbracelet/lipgloss v1.1.0

go.sum

+6-6
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR
7474
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
7575
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
7676
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
77-
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
78-
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
79-
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
80-
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
77+
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
78+
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
79+
github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
80+
github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
8181
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
8282
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
8383
github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
@@ -90,8 +90,8 @@ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2ll
9090
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
9191
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
9292
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
93-
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
94-
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
93+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
94+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
9595
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
9696
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
9797
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=

internal/app/app.go

+60
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package app
33
import (
44
"context"
55
"database/sql"
6+
"errors"
7+
"fmt"
68
"maps"
9+
"strings"
710
"sync"
811
"time"
912

@@ -73,6 +76,63 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
7376
return app, nil
7477
}
7578

79+
// RunNonInteractive handles the execution flow when a prompt is provided via CLI flag.
80+
func (a *App) RunNonInteractive(ctx context.Context, prompt string) error {
81+
logging.Info("Running in non-interactive mode")
82+
83+
const maxPromptLengthForTitle = 100
84+
titlePrefix := "Non-interactive: "
85+
var titleSuffix string
86+
87+
if len(prompt) > maxPromptLengthForTitle {
88+
titleSuffix = prompt[:maxPromptLengthForTitle] + "..."
89+
} else {
90+
titleSuffix = prompt
91+
}
92+
title := titlePrefix + titleSuffix
93+
94+
sess, err := a.Sessions.Create(ctx, title)
95+
if err != nil {
96+
return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
97+
}
98+
logging.Info("Created session for non-interactive run", "session_id", sess.ID)
99+
100+
// Automatically approve all permission requests for this non-interactive session
101+
a.Permissions.AutoApproveSession(sess.ID)
102+
103+
done, err := a.CoderAgent.Run(ctx, sess.ID, prompt)
104+
if err != nil {
105+
return fmt.Errorf("failed to start agent processing stream: %w", err)
106+
}
107+
108+
result := <-done
109+
if result.Err() != nil {
110+
if errors.Is(result.Err(), context.Canceled) || errors.Is(result.Err(), agent.ErrRequestCancelled) {
111+
logging.Info("Agent processing cancelled", "session_id", sess.ID)
112+
return nil
113+
}
114+
return fmt.Errorf("agent processing failed: %w", result.Err())
115+
}
116+
117+
response := result.Response()
118+
119+
// Use a strings.Builder to accumulate the text parts
120+
var builder strings.Builder
121+
for _, part := range response.Parts {
122+
if textPart, ok := part.(message.TextContent); ok {
123+
builder.WriteString(textPart.Text)
124+
}
125+
}
126+
127+
// Print the final accumulated text
128+
if builder.Len() > 0 {
129+
fmt.Println(builder.String())
130+
}
131+
132+
logging.Info("Non-interactive run completed", "session_id", sess.ID)
133+
134+
return nil
135+
}
76136

77137
// Shutdown performs a clean shutdown of the application
78138
func (app *App) Shutdown() {

0 commit comments

Comments
 (0)