Skip to content

Commit 103f1c1

Browse files
radutopalakujtimiihoxha
authored andcommitted
feat: non-interactive mode
1 parent 9e065cd commit 103f1c1

File tree

8 files changed

+374
-19
lines changed

8 files changed

+374
-19
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

README.md

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ OpenCode supports a variety of AI models from different providers:
235235

236236
- Gemini 2.5
237237
- Gemini 2.5 Flash
238-
238+
239239
## Usage
240240

241241
```bash
@@ -249,13 +249,46 @@ opencode -d
249249
opencode -c /path/to/project
250250
```
251251

252+
## Non-interactive Prompt Mode
253+
254+
You can run OpenCode in non-interactive mode by passing a prompt directly as a command-line argument. This is useful for scripting, automation, or when you want a quick answer without launching the full TUI.
255+
256+
```bash
257+
# Run a single prompt and print the AI's response to the terminal
258+
opencode -p "Explain the use of context in Go"
259+
260+
# Get response in JSON format
261+
opencode -p "Explain the use of context in Go" -f json
262+
263+
# Run without showing the spinner (useful for scripts)
264+
opencode -p "Explain the use of context in Go" -q
265+
```
266+
267+
In this mode, OpenCode will process your prompt, print the result to standard output, and then exit. All permissions are auto-approved for the session.
268+
269+
By default, a spinner animation is displayed while the model is processing your query. You can disable this spinner with the `-q` or `--quiet` flag, which is particularly useful when running OpenCode from scripts or automated workflows.
270+
271+
### Output Formats
272+
273+
OpenCode supports the following output formats in non-interactive mode:
274+
275+
| Format | Description |
276+
| ------ | -------------------------------------- |
277+
| `text` | Plain text output (default) |
278+
| `json` | Output wrapped in a JSON object |
279+
280+
The output format is implemented as a strongly-typed `OutputFormat` in the codebase, ensuring type safety and validation when processing outputs.
281+
252282
## Command-line Flags
253283

254-
| Flag | Short | Description |
255-
| --------- | ----- | ----------------------------- |
256-
| `--help` | `-h` | Display help information |
257-
| `--debug` | `-d` | Enable debug mode |
258-
| `--cwd` | `-c` | Set current working directory |
284+
| Flag | Short | Description |
285+
| ----------------- | ----- | ------------------------------------------------------ |
286+
| `--help` | `-h` | Display help information |
287+
| `--debug` | `-d` | Enable debug mode |
288+
| `--cwd` | `-c` | Set current working directory |
289+
| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
290+
| `--output-format` | `-f` | Output format for non-interactive mode (text, json) |
291+
| `--quiet` | `-q` | Hide spinner in non-interactive mode |
259292

260293
## Keyboard Shortcuts
261294

@@ -390,6 +423,7 @@ Custom commands are predefined prompts stored as Markdown files in one of three
390423
```
391424

392425
2. **Project Commands** (prefixed with `project:`):
426+
393427
```
394428
<PROJECT DIR>/.opencode/commands/
395429
```
@@ -420,6 +454,7 @@ RUN grep -R "$SEARCH_PATTERN" $DIRECTORY
420454
```
421455

422456
When you run a command with arguments, OpenCode will prompt you to enter values for each unique placeholder. Named arguments provide several benefits:
457+
423458
- Clear identification of what each argument represents
424459
- Ability to use the same argument multiple times
425460
- Better organization for commands with multiple inputs

cmd/root.go

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/opencode-ai/opencode/internal/app"
1313
"github.com/opencode-ai/opencode/internal/config"
1414
"github.com/opencode-ai/opencode/internal/db"
15+
"github.com/opencode-ai/opencode/internal/format"
1516
"github.com/opencode-ai/opencode/internal/llm/agent"
1617
"github.com/opencode-ai/opencode/internal/logging"
1718
"github.com/opencode-ai/opencode/internal/pubsub"
@@ -21,11 +22,30 @@ import (
2122
)
2223

2324
var rootCmd = &cobra.Command{
24-
Use: "OpenCode",
25-
Short: "A terminal AI assistant for software development",
25+
Use: "opencode",
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 -p "Explain the use of context in Go"
45+
46+
# Run a single non-interactive prompt with JSON output format
47+
opencode -p "Explain the use of context in Go" -f json
48+
`,
2949
RunE: func(cmd *cobra.Command, args []string) error {
3050
// If the help flag is set, show the help message
3151
if cmd.Flag("help").Changed {
@@ -40,6 +60,15 @@ to assist developers in writing, debugging, and understanding code directly from
4060
// Load the config
4161
debug, _ := cmd.Flags().GetBool("debug")
4262
cwd, _ := cmd.Flags().GetString("cwd")
63+
prompt, _ := cmd.Flags().GetString("prompt")
64+
outputFormat, _ := cmd.Flags().GetString("output-format")
65+
quiet, _ := cmd.Flags().GetBool("quiet")
66+
67+
// Validate format option
68+
if !format.IsValid(outputFormat) {
69+
return fmt.Errorf("invalid format option: %s\n%s", outputFormat, format.GetHelpText())
70+
}
71+
4372
if cwd != "" {
4473
err := os.Chdir(cwd)
4574
if err != nil {
@@ -73,17 +102,26 @@ to assist developers in writing, debugging, and understanding code directly from
73102
logging.Error("Failed to create app: %v", err)
74103
return err
75104
}
105+
// Defer shutdown here so it runs for both interactive and non-interactive modes
106+
defer app.Shutdown()
76107

108+
// Initialize MCP tools early for both modes
109+
initMCPTools(ctx, app)
110+
111+
// Non-interactive mode
112+
if prompt != "" {
113+
// Run non-interactive flow using the App method
114+
return app.RunNonInteractive(ctx, prompt, outputFormat, quiet)
115+
}
116+
117+
// Interactive mode
77118
// Set up the TUI
78119
zone.NewGlobal()
79120
program := tea.NewProgram(
80121
tui.New(app),
81122
tea.WithAltScreen(),
82123
)
83124

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

@@ -255,4 +293,17 @@ func init() {
255293
rootCmd.Flags().BoolP("version", "v", false, "Version")
256294
rootCmd.Flags().BoolP("debug", "d", false, "Debug")
257295
rootCmd.Flags().StringP("cwd", "c", "", "Current working directory")
296+
rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
297+
298+
// Add format flag with validation logic
299+
rootCmd.Flags().StringP("output-format", "f", format.Text.String(),
300+
"Output format for non-interactive mode (text, json)")
301+
302+
// Add quiet flag to hide spinner in non-interactive mode
303+
rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
304+
305+
// Register custom validation for the format flag
306+
rootCmd.RegisterFlagCompletionFunc("output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
307+
return format.SupportedFormats, cobra.ShellCompDirectiveNoFileComp
308+
})
258309
}

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ require (
1111
github.com/aymanbagabas/go-udiff v0.2.0
1212
github.com/bmatcuk/doublestar/v4 v4.8.1
1313
github.com/catppuccin/go v0.3.0
14-
github.com/charmbracelet/bubbles v0.20.0
15-
github.com/charmbracelet/bubbletea v1.3.4
14+
github.com/charmbracelet/bubbles v0.21.0
15+
github.com/charmbracelet/bubbletea v1.3.5
1616
github.com/charmbracelet/glamour v0.9.1
1717
github.com/charmbracelet/lipgloss v1.1.0
1818
github.com/charmbracelet/x/ansi v0.8.0

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR
6868
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
6969
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
7070
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
71-
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
72-
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
73-
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
74-
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
71+
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
72+
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
73+
github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
74+
github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
7575
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
7676
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
7777
github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
@@ -82,8 +82,8 @@ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2ll
8282
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
8383
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
8484
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
85-
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
86-
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
85+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
86+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
8787
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
8888
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
8989
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=

internal/app/app.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ package app
33
import (
44
"context"
55
"database/sql"
6+
"errors"
7+
"fmt"
68
"maps"
79
"sync"
810
"time"
911

1012
"github.com/opencode-ai/opencode/internal/config"
1113
"github.com/opencode-ai/opencode/internal/db"
14+
"github.com/opencode-ai/opencode/internal/format"
1215
"github.com/opencode-ai/opencode/internal/history"
1316
"github.com/opencode-ai/opencode/internal/llm/agent"
1417
"github.com/opencode-ai/opencode/internal/logging"
@@ -93,6 +96,70 @@ 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, outputFormat string, quiet bool) error {
101+
logging.Info("Running in non-interactive mode")
102+
103+
// Start spinner if not in quiet mode
104+
var spinner *format.Spinner
105+
if !quiet {
106+
spinner = format.NewSpinner("Thinking...")
107+
spinner.Start()
108+
defer spinner.Stop()
109+
}
110+
111+
const maxPromptLengthForTitle = 100
112+
titlePrefix := "Non-interactive: "
113+
var titleSuffix string
114+
115+
if len(prompt) > maxPromptLengthForTitle {
116+
titleSuffix = prompt[:maxPromptLengthForTitle] + "..."
117+
} else {
118+
titleSuffix = prompt
119+
}
120+
title := titlePrefix + titleSuffix
121+
122+
sess, err := a.Sessions.Create(ctx, title)
123+
if err != nil {
124+
return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
125+
}
126+
logging.Info("Created session for non-interactive run", "session_id", sess.ID)
127+
128+
// Automatically approve all permission requests for this non-interactive session
129+
a.Permissions.AutoApproveSession(sess.ID)
130+
131+
done, err := a.CoderAgent.Run(ctx, sess.ID, prompt)
132+
if err != nil {
133+
return fmt.Errorf("failed to start agent processing stream: %w", err)
134+
}
135+
136+
result := <-done
137+
if result.Error != nil {
138+
if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
139+
logging.Info("Agent processing cancelled", "session_id", sess.ID)
140+
return nil
141+
}
142+
return fmt.Errorf("agent processing failed: %w", result.Error)
143+
}
144+
145+
// Stop spinner before printing output
146+
if !quiet && spinner != nil {
147+
spinner.Stop()
148+
}
149+
150+
// Get the text content from the response
151+
content := "No content available"
152+
if result.Message.Content().String() != "" {
153+
content = result.Message.Content().String()
154+
}
155+
156+
fmt.Println(format.FormatOutput(content, outputFormat))
157+
158+
logging.Info("Non-interactive run completed", "session_id", sess.ID)
159+
160+
return nil
161+
}
162+
96163
// Shutdown performs a clean shutdown of the application
97164
func (app *App) Shutdown() {
98165
// Cancel all watcher goroutines

0 commit comments

Comments
 (0)