feat: non-interactive mode
This commit is contained in:
parent
9e065cdf1f
commit
103f1c1183
8 changed files with 374 additions and 19 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -43,3 +43,4 @@ Thumbs.db
|
||||||
|
|
||||||
.opencode/
|
.opencode/
|
||||||
|
|
||||||
|
opencode
|
||||||
|
|
|
||||||
47
README.md
47
README.md
|
|
@ -235,7 +235,7 @@ OpenCode supports a variety of AI models from different providers:
|
||||||
|
|
||||||
- Gemini 2.5
|
- Gemini 2.5
|
||||||
- Gemini 2.5 Flash
|
- Gemini 2.5 Flash
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -249,13 +249,46 @@ opencode -d
|
||||||
opencode -c /path/to/project
|
opencode -c /path/to/project
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Non-interactive Prompt Mode
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run a single prompt and print the AI's response to the terminal
|
||||||
|
opencode -p "Explain the use of context in Go"
|
||||||
|
|
||||||
|
# Get response in JSON format
|
||||||
|
opencode -p "Explain the use of context in Go" -f json
|
||||||
|
|
||||||
|
# Run without showing the spinner (useful for scripts)
|
||||||
|
opencode -p "Explain the use of context in Go" -q
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Output Formats
|
||||||
|
|
||||||
|
OpenCode supports the following output formats in non-interactive mode:
|
||||||
|
|
||||||
|
| Format | Description |
|
||||||
|
| ------ | -------------------------------------- |
|
||||||
|
| `text` | Plain text output (default) |
|
||||||
|
| `json` | Output wrapped in a JSON object |
|
||||||
|
|
||||||
|
The output format is implemented as a strongly-typed `OutputFormat` in the codebase, ensuring type safety and validation when processing outputs.
|
||||||
|
|
||||||
## Command-line Flags
|
## Command-line Flags
|
||||||
|
|
||||||
| Flag | Short | Description |
|
| Flag | Short | Description |
|
||||||
| --------- | ----- | ----------------------------- |
|
| ----------------- | ----- | ------------------------------------------------------ |
|
||||||
| `--help` | `-h` | Display help information |
|
| `--help` | `-h` | Display help information |
|
||||||
| `--debug` | `-d` | Enable debug mode |
|
| `--debug` | `-d` | Enable debug mode |
|
||||||
| `--cwd` | `-c` | Set current working directory |
|
| `--cwd` | `-c` | Set current working directory |
|
||||||
|
| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
|
||||||
|
| `--output-format` | `-f` | Output format for non-interactive mode (text, json) |
|
||||||
|
| `--quiet` | `-q` | Hide spinner in non-interactive mode |
|
||||||
|
|
||||||
## Keyboard Shortcuts
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
|
@ -390,6 +423,7 @@ Custom commands are predefined prompts stored as Markdown files in one of three
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Project Commands** (prefixed with `project:`):
|
2. **Project Commands** (prefixed with `project:`):
|
||||||
|
|
||||||
```
|
```
|
||||||
<PROJECT DIR>/.opencode/commands/
|
<PROJECT DIR>/.opencode/commands/
|
||||||
```
|
```
|
||||||
|
|
@ -420,6 +454,7 @@ RUN grep -R "$SEARCH_PATTERN" $DIRECTORY
|
||||||
```
|
```
|
||||||
|
|
||||||
When you run a command with arguments, OpenCode will prompt you to enter values for each unique placeholder. Named arguments provide several benefits:
|
When you run a command with arguments, OpenCode will prompt you to enter values for each unique placeholder. Named arguments provide several benefits:
|
||||||
|
|
||||||
- Clear identification of what each argument represents
|
- Clear identification of what each argument represents
|
||||||
- Ability to use the same argument multiple times
|
- Ability to use the same argument multiple times
|
||||||
- Better organization for commands with multiple inputs
|
- Better organization for commands with multiple inputs
|
||||||
|
|
|
||||||
61
cmd/root.go
61
cmd/root.go
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/opencode-ai/opencode/internal/app"
|
"github.com/opencode-ai/opencode/internal/app"
|
||||||
"github.com/opencode-ai/opencode/internal/config"
|
"github.com/opencode-ai/opencode/internal/config"
|
||||||
"github.com/opencode-ai/opencode/internal/db"
|
"github.com/opencode-ai/opencode/internal/db"
|
||||||
|
"github.com/opencode-ai/opencode/internal/format"
|
||||||
"github.com/opencode-ai/opencode/internal/llm/agent"
|
"github.com/opencode-ai/opencode/internal/llm/agent"
|
||||||
"github.com/opencode-ai/opencode/internal/logging"
|
"github.com/opencode-ai/opencode/internal/logging"
|
||||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||||
|
|
@ -21,11 +22,30 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "OpenCode",
|
Use: "opencode",
|
||||||
Short: "A terminal AI assistant for software development",
|
Short: "Terminal-based AI assistant for software development",
|
||||||
Long: `OpenCode is a powerful terminal-based AI assistant that helps with software development tasks.
|
Long: `OpenCode is a powerful terminal-based AI assistant that helps with software development tasks.
|
||||||
It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
|
It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
|
||||||
to assist developers in writing, debugging, and understanding code directly from the terminal.`,
|
to assist developers in writing, debugging, and understanding code directly from the terminal.`,
|
||||||
|
Example: `
|
||||||
|
# Run in interactive mode
|
||||||
|
opencode
|
||||||
|
|
||||||
|
# Run with debug logging
|
||||||
|
opencode -d
|
||||||
|
|
||||||
|
# Run with debug logging in a specific directory
|
||||||
|
opencode -d -c /path/to/project
|
||||||
|
|
||||||
|
# Print version
|
||||||
|
opencode -v
|
||||||
|
|
||||||
|
# Run a single non-interactive prompt
|
||||||
|
opencode -p "Explain the use of context in Go"
|
||||||
|
|
||||||
|
# Run a single non-interactive prompt with JSON output format
|
||||||
|
opencode -p "Explain the use of context in Go" -f json
|
||||||
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
// If the help flag is set, show the help message
|
// If the help flag is set, show the help message
|
||||||
if cmd.Flag("help").Changed {
|
if cmd.Flag("help").Changed {
|
||||||
|
|
@ -40,6 +60,15 @@ to assist developers in writing, debugging, and understanding code directly from
|
||||||
// Load the config
|
// Load the config
|
||||||
debug, _ := cmd.Flags().GetBool("debug")
|
debug, _ := cmd.Flags().GetBool("debug")
|
||||||
cwd, _ := cmd.Flags().GetString("cwd")
|
cwd, _ := cmd.Flags().GetString("cwd")
|
||||||
|
prompt, _ := cmd.Flags().GetString("prompt")
|
||||||
|
outputFormat, _ := cmd.Flags().GetString("output-format")
|
||||||
|
quiet, _ := cmd.Flags().GetBool("quiet")
|
||||||
|
|
||||||
|
// Validate format option
|
||||||
|
if !format.IsValid(outputFormat) {
|
||||||
|
return fmt.Errorf("invalid format option: %s\n%s", outputFormat, format.GetHelpText())
|
||||||
|
}
|
||||||
|
|
||||||
if cwd != "" {
|
if cwd != "" {
|
||||||
err := os.Chdir(cwd)
|
err := os.Chdir(cwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -73,7 +102,19 @@ to assist developers in writing, debugging, and understanding code directly from
|
||||||
logging.Error("Failed to create app: %v", err)
|
logging.Error("Failed to create app: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Defer shutdown here so it runs for both interactive and non-interactive modes
|
||||||
|
defer app.Shutdown()
|
||||||
|
|
||||||
|
// Initialize MCP tools early for both modes
|
||||||
|
initMCPTools(ctx, app)
|
||||||
|
|
||||||
|
// Non-interactive mode
|
||||||
|
if prompt != "" {
|
||||||
|
// Run non-interactive flow using the App method
|
||||||
|
return app.RunNonInteractive(ctx, prompt, outputFormat, quiet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interactive mode
|
||||||
// Set up the TUI
|
// Set up the TUI
|
||||||
zone.NewGlobal()
|
zone.NewGlobal()
|
||||||
program := tea.NewProgram(
|
program := tea.NewProgram(
|
||||||
|
|
@ -81,9 +122,6 @@ to assist developers in writing, debugging, and understanding code directly from
|
||||||
tea.WithAltScreen(),
|
tea.WithAltScreen(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize MCP tools in the background
|
|
||||||
initMCPTools(ctx, app)
|
|
||||||
|
|
||||||
// Setup the subscriptions, this will send services events to the TUI
|
// Setup the subscriptions, this will send services events to the TUI
|
||||||
ch, cancelSubs := setupSubscriptions(app, ctx)
|
ch, cancelSubs := setupSubscriptions(app, ctx)
|
||||||
|
|
||||||
|
|
@ -255,4 +293,17 @@ func init() {
|
||||||
rootCmd.Flags().BoolP("version", "v", false, "Version")
|
rootCmd.Flags().BoolP("version", "v", false, "Version")
|
||||||
rootCmd.Flags().BoolP("debug", "d", false, "Debug")
|
rootCmd.Flags().BoolP("debug", "d", false, "Debug")
|
||||||
rootCmd.Flags().StringP("cwd", "c", "", "Current working directory")
|
rootCmd.Flags().StringP("cwd", "c", "", "Current working directory")
|
||||||
|
rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
|
||||||
|
|
||||||
|
// Add format flag with validation logic
|
||||||
|
rootCmd.Flags().StringP("output-format", "f", format.Text.String(),
|
||||||
|
"Output format for non-interactive mode (text, json)")
|
||||||
|
|
||||||
|
// Add quiet flag to hide spinner in non-interactive mode
|
||||||
|
rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
|
||||||
|
|
||||||
|
// Register custom validation for the format flag
|
||||||
|
rootCmd.RegisterFlagCompletionFunc("output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return format.SupportedFormats, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -11,8 +11,8 @@ require (
|
||||||
github.com/aymanbagabas/go-udiff v0.2.0
|
github.com/aymanbagabas/go-udiff v0.2.0
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
github.com/bmatcuk/doublestar/v4 v4.8.1
|
||||||
github.com/catppuccin/go v0.3.0
|
github.com/catppuccin/go v0.3.0
|
||||||
github.com/charmbracelet/bubbles v0.20.0
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.4
|
github.com/charmbracelet/bubbletea v1.3.5
|
||||||
github.com/charmbracelet/glamour v0.9.1
|
github.com/charmbracelet/glamour v0.9.1
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/x/ansi v0.8.0
|
github.com/charmbracelet/x/ansi v0.8.0
|
||||||
|
|
|
||||||
12
go.sum
12
go.sum
|
|
@ -68,10 +68,10 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
|
github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
|
||||||
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
|
github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
|
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
|
||||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,15 @@ package app
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/opencode-ai/opencode/internal/config"
|
"github.com/opencode-ai/opencode/internal/config"
|
||||||
"github.com/opencode-ai/opencode/internal/db"
|
"github.com/opencode-ai/opencode/internal/db"
|
||||||
|
"github.com/opencode-ai/opencode/internal/format"
|
||||||
"github.com/opencode-ai/opencode/internal/history"
|
"github.com/opencode-ai/opencode/internal/history"
|
||||||
"github.com/opencode-ai/opencode/internal/llm/agent"
|
"github.com/opencode-ai/opencode/internal/llm/agent"
|
||||||
"github.com/opencode-ai/opencode/internal/logging"
|
"github.com/opencode-ai/opencode/internal/logging"
|
||||||
|
|
@ -93,6 +96,70 @@ func (app *App) initTheme() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunNonInteractive handles the execution flow when a prompt is provided via CLI flag.
|
||||||
|
func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat string, quiet bool) error {
|
||||||
|
logging.Info("Running in non-interactive mode")
|
||||||
|
|
||||||
|
// Start spinner if not in quiet mode
|
||||||
|
var spinner *format.Spinner
|
||||||
|
if !quiet {
|
||||||
|
spinner = format.NewSpinner("Thinking...")
|
||||||
|
spinner.Start()
|
||||||
|
defer spinner.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxPromptLengthForTitle = 100
|
||||||
|
titlePrefix := "Non-interactive: "
|
||||||
|
var titleSuffix string
|
||||||
|
|
||||||
|
if len(prompt) > maxPromptLengthForTitle {
|
||||||
|
titleSuffix = prompt[:maxPromptLengthForTitle] + "..."
|
||||||
|
} else {
|
||||||
|
titleSuffix = prompt
|
||||||
|
}
|
||||||
|
title := titlePrefix + titleSuffix
|
||||||
|
|
||||||
|
sess, err := a.Sessions.Create(ctx, title)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
|
||||||
|
}
|
||||||
|
logging.Info("Created session for non-interactive run", "session_id", sess.ID)
|
||||||
|
|
||||||
|
// Automatically approve all permission requests for this non-interactive session
|
||||||
|
a.Permissions.AutoApproveSession(sess.ID)
|
||||||
|
|
||||||
|
done, err := a.CoderAgent.Run(ctx, sess.ID, prompt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start agent processing stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := <-done
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
|
||||||
|
logging.Info("Agent processing cancelled", "session_id", sess.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("agent processing failed: %w", result.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop spinner before printing output
|
||||||
|
if !quiet && spinner != nil {
|
||||||
|
spinner.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the text content from the response
|
||||||
|
content := "No content available"
|
||||||
|
if result.Message.Content().String() != "" {
|
||||||
|
content = result.Message.Content().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(format.FormatOutput(content, outputFormat))
|
||||||
|
|
||||||
|
logging.Info("Non-interactive run completed", "session_id", sess.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Shutdown performs a clean shutdown of the application
|
// Shutdown performs a clean shutdown of the application
|
||||||
func (app *App) Shutdown() {
|
func (app *App) Shutdown() {
|
||||||
// Cancel all watcher goroutines
|
// Cancel all watcher goroutines
|
||||||
|
|
|
||||||
99
internal/format/format.go
Normal file
99
internal/format/format.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OutputFormat represents the output format type for non-interactive mode
|
||||||
|
type OutputFormat string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Text format outputs the AI response as plain text.
|
||||||
|
Text OutputFormat = "text"
|
||||||
|
|
||||||
|
// JSON format outputs the AI response wrapped in a JSON object.
|
||||||
|
JSON OutputFormat = "json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the string representation of the OutputFormat
|
||||||
|
func (f OutputFormat) String() string {
|
||||||
|
return string(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportedFormats is a list of all supported output formats as strings
|
||||||
|
var SupportedFormats = []string{
|
||||||
|
string(Text),
|
||||||
|
string(JSON),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse converts a string to an OutputFormat
|
||||||
|
func Parse(s string) (OutputFormat, error) {
|
||||||
|
s = strings.ToLower(strings.TrimSpace(s))
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case string(Text):
|
||||||
|
return Text, nil
|
||||||
|
case string(JSON):
|
||||||
|
return JSON, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("invalid format: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid checks if the provided format string is supported
|
||||||
|
func IsValid(s string) bool {
|
||||||
|
_, err := Parse(s)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHelpText returns a formatted string describing all supported formats
|
||||||
|
func GetHelpText() string {
|
||||||
|
return fmt.Sprintf(`Supported output formats:
|
||||||
|
- %s: Plain text output (default)
|
||||||
|
- %s: Output wrapped in a JSON object`,
|
||||||
|
Text, JSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatOutput formats the AI response according to the specified format
|
||||||
|
func FormatOutput(content string, formatStr string) string {
|
||||||
|
format, err := Parse(formatStr)
|
||||||
|
if err != nil {
|
||||||
|
// Default to text format on error
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case JSON:
|
||||||
|
return formatAsJSON(content)
|
||||||
|
case Text:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatAsJSON wraps the content in a simple JSON object
|
||||||
|
func formatAsJSON(content string) string {
|
||||||
|
// Use the JSON package to properly escape the content
|
||||||
|
response := struct {
|
||||||
|
Response string `json:"response"`
|
||||||
|
}{
|
||||||
|
Response: content,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes, err := json.MarshalIndent(response, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
// In case of an error, return a manually formatted JSON
|
||||||
|
jsonEscaped := strings.Replace(content, "\\", "\\\\", -1)
|
||||||
|
jsonEscaped = strings.Replace(jsonEscaped, "\"", "\\\"", -1)
|
||||||
|
jsonEscaped = strings.Replace(jsonEscaped, "\n", "\\n", -1)
|
||||||
|
jsonEscaped = strings.Replace(jsonEscaped, "\r", "\\r", -1)
|
||||||
|
jsonEscaped = strings.Replace(jsonEscaped, "\t", "\\t", -1)
|
||||||
|
|
||||||
|
return fmt.Sprintf("{\n \"response\": \"%s\"\n}", jsonEscaped)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(jsonBytes)
|
||||||
|
}
|
||||||
102
internal/format/spinner.go
Normal file
102
internal/format/spinner.go
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Spinner wraps the bubbles spinner for non-interactive mode
|
||||||
|
type Spinner struct {
|
||||||
|
model spinner.Model
|
||||||
|
done chan struct{}
|
||||||
|
prog *tea.Program
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// spinnerModel is the tea.Model for the spinner
|
||||||
|
type spinnerModel struct {
|
||||||
|
spinner spinner.Model
|
||||||
|
message string
|
||||||
|
quitting bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m spinnerModel) Init() tea.Cmd {
|
||||||
|
return m.spinner.Tick
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
m.quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
case spinner.TickMsg:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.spinner, cmd = m.spinner.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
case quitMsg:
|
||||||
|
m.quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
default:
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m spinnerModel) View() string {
|
||||||
|
if m.quitting {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s", m.spinner.View(), m.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// quitMsg is sent when we want to quit the spinner
|
||||||
|
type quitMsg struct{}
|
||||||
|
|
||||||
|
// NewSpinner creates a new spinner with the given message
|
||||||
|
func NewSpinner(message string) *Spinner {
|
||||||
|
s := spinner.New()
|
||||||
|
s.Spinner = spinner.Dot
|
||||||
|
s.Style = s.Style.Foreground(s.Style.GetForeground())
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
model := spinnerModel{
|
||||||
|
spinner: s,
|
||||||
|
message: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
|
||||||
|
|
||||||
|
return &Spinner{
|
||||||
|
model: s,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
prog: prog,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start begins the spinner animation
|
||||||
|
func (s *Spinner) Start() {
|
||||||
|
go func() {
|
||||||
|
defer close(s.done)
|
||||||
|
go func() {
|
||||||
|
<-s.ctx.Done()
|
||||||
|
s.prog.Send(quitMsg{})
|
||||||
|
}()
|
||||||
|
_, err := s.prog.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop ends the spinner animation
|
||||||
|
func (s *Spinner) Stop() {
|
||||||
|
s.cancel()
|
||||||
|
<-s.done
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue