Skip to content

Commit 39ceaf4

Browse files
radutopalaRadu Topala
authored and
Radu Topala
committed
feat: non-interactive mode
1 parent 603a3e3 commit 39ceaf4

File tree

5 files changed

+106
-13
lines changed

5 files changed

+106
-13
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ Thumbs.db
4343

4444
.opencode/
4545

46+
opencode

cmd/root.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,28 @@ import (
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,17 +95,26 @@ 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()
76100

101+
// Initialize MCP tools early for both modes
102+
initMCPTools(ctx, app)
103+
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(
80114
tui.New(app),
81115
tea.WithAltScreen(),
82116
)
83117

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

go.mod

Lines changed: 2 additions & 2 deletions
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

Lines changed: 6 additions & 6 deletions
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

Lines changed: 61 additions & 0 deletions
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

@@ -93,6 +96,64 @@ func (app *App) initTheme() {
9396
}
9497
}
9598

99+
// RunNonInteractive handles the execution flow when a prompt is provided via CLI flag.
100+
func (a *App) RunNonInteractive(ctx context.Context, prompt string) error {
101+
logging.Info("Running in non-interactive mode")
102+
103+
const maxPromptLengthForTitle = 100
104+
titlePrefix := "Non-interactive: "
105+
var titleSuffix string
106+
107+
if len(prompt) > maxPromptLengthForTitle {
108+
titleSuffix = prompt[:maxPromptLengthForTitle] + "..."
109+
} else {
110+
titleSuffix = prompt
111+
}
112+
title := titlePrefix + titleSuffix
113+
114+
sess, err := a.Sessions.Create(ctx, title)
115+
if err != nil {
116+
return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
117+
}
118+
logging.Info("Created session for non-interactive run", "session_id", sess.ID)
119+
120+
// Automatically approve all permission requests for this non-interactive session
121+
a.Permissions.AutoApproveSession(sess.ID)
122+
123+
done, err := a.CoderAgent.Run(ctx, sess.ID, prompt)
124+
if err != nil {
125+
return fmt.Errorf("failed to start agent processing stream: %w", err)
126+
}
127+
128+
result := <-done
129+
if result.Err() != nil {
130+
if errors.Is(result.Err(), context.Canceled) || errors.Is(result.Err(), agent.ErrRequestCancelled) {
131+
logging.Info("Agent processing cancelled", "session_id", sess.ID)
132+
return nil
133+
}
134+
return fmt.Errorf("agent processing failed: %w", result.Err())
135+
}
136+
137+
response := result.Response()
138+
139+
// Use a strings.Builder to accumulate the text parts
140+
var builder strings.Builder
141+
for _, part := range response.Parts {
142+
if textPart, ok := part.(message.TextContent); ok {
143+
builder.WriteString(textPart.Text)
144+
}
145+
}
146+
147+
// Print the final accumulated text
148+
if builder.Len() > 0 {
149+
fmt.Println(builder.String())
150+
}
151+
152+
logging.Info("Non-interactive run completed", "session_id", sess.ID)
153+
154+
return nil
155+
}
156+
96157
// Shutdown performs a clean shutdown of the application
97158
func (app *App) Shutdown() {
98159
// Cancel all watcher goroutines

0 commit comments

Comments
 (0)