Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ See the [go install](https://go.dev/ref/mod#go-install) instructions for more in
To use `gemini-cli`, you'll need an API key set in the `GEMINI_API_KEY` environment variable.
If you don't already have one, create a key in [Google AI Studio](https://makersuite.google.com/app/apikey).

To set the environment variable in the terminal:
```console
Set the environment variable in the terminal:
```sh
export GEMINI_API_KEY=<your_api_key>
```

Expand All @@ -51,8 +51,9 @@ A short list of supported system commands:
| !h | Select a history operation <sup>3</sup> |

<sup>1</sup> System instruction (also known as "system prompt") is a more forceful prompt to the model.
The model will adhere the instructions more strongly than if they appeared in a normal prompt.
The system instructions must be specified by the user in the [configuration file](#configuration-file).
The model will follow instructions more closely than with a standard prompt.
The user must specify system instructions in the [configuration file](#configuration-file).
Note that not all generative models support them.

<sup>2</sup> Model operations:
* Select a generative model from the list of available models
Expand Down
2 changes: 1 addition & 1 deletion cmd/gemini/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func run() int {
return err
}

chatHandler, err := chat.New(getCurrentUser(), chatSession, configuration, os.Stdout, &opts)
chatHandler, err := chat.New(getCurrentUser(), chatSession, configuration, &opts)
if err != nil {
return err
}
Expand Down
132 changes: 36 additions & 96 deletions internal/chat/chat.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
package chat

import (
"errors"
"fmt"
"io"
"strings"
"time"

"github.com/chzyer/readline"
"github.com/reugn/gemini-cli/gemini"
"github.com/reugn/gemini-cli/internal/cli"
"github.com/reugn/gemini-cli/internal/config"
"github.com/reugn/gemini-cli/internal/handler"
"github.com/reugn/gemini-cli/internal/terminal"
)

// Chat handles the interactive exchange of messages between user and model.
type Chat struct {
reader *readline.Instance
writer io.Writer
terminalPrompt *cli.Prompt
opts *Opts
io *terminal.IO

geminiHandler handler.MessageHandler
systemHandler handler.MessageHandler
Expand All @@ -28,119 +19,68 @@ type Chat struct {
// New returns a new Chat.
func New(
user string, session *gemini.ChatSession,
configuration *config.Configuration, writer io.Writer, opts *Opts,
configuration *config.Configuration, opts *Opts,
) (*Chat, error) {
reader, err := readline.NewEx(&readline.Config{})
if err != nil {
return nil, err
terminalIOConfig := &terminal.IOConfig{
User: user,
Multiline: opts.Multiline,
LineTerminator: opts.LineTerminator,
}

terminalPrompt := cli.NewPrompt(user)
reader.SetPrompt(terminalPrompt.User)
if opts.Multiline {
// disable history for multiline input mode
reader.HistoryDisable()
terminalIO, err := terminal.NewIO(terminalIOConfig)
if err != nil {
return nil, err
}

spinner := cli.NewSpinner(writer, time.Second, 5)
geminiHandler, err := handler.NewGeminiQuery(session, spinner, opts.Style)
geminiIO := handler.NewIO(terminalIO, terminalIO.Prompt.Gemini)
geminiHandler, err := handler.NewGeminiQuery(geminiIO, session, opts.Style)
if err != nil {
return nil, err
}

systemHandler := handler.NewSystemCommand(session, configuration, reader,
&opts.Multiline, opts.GenerativeModel)
systemIO := handler.NewIO(terminalIO, terminalIO.Prompt.Cli)
systemHandler := handler.NewSystemCommand(systemIO, session, configuration,
opts.GenerativeModel)

return &Chat{
terminalPrompt: terminalPrompt,
reader: reader,
writer: writer,
opts: opts,
geminiHandler: geminiHandler,
systemHandler: systemHandler,
io: terminalIO,
geminiHandler: geminiHandler,
systemHandler: systemHandler,
}, nil
}

// Start starts the main chat loop between user and model.
func (c *Chat) Start() {
for {
message, ok := c.read()
if !ok {
// read query from the user
message := c.io.Read()
if message == "" {
continue
}

// process the message
messageHandler, terminalPrompt := c.getHandlerPrompt(message)
response, quit := messageHandler.Handle(message)
_ = response.Print(c.writer, terminalPrompt)

if quit {
break
}
}
}
// get handler for the read message
// the message is not empty here
messageHandler := c.getHandler(message[:1])

func (c *Chat) read() (string, bool) {
if c.opts.Multiline {
return c.readMultiLine()
}
return c.readLine()
}
// write the agent terminal prompt
c.io.Write(messageHandler.TerminalPrompt())

func (c *Chat) readLine() (string, bool) {
input, err := c.reader.Readline()
if err != nil {
return c.handleReadError(len(input), err)
}
return validateInput(input)
}
// process the message
response, quit := messageHandler.Handle(message)

func (c *Chat) readMultiLine() (string, bool) {
var builder strings.Builder
term := c.opts.LineTerminator
for {
input, err := c.reader.Readline()
if err != nil {
c.reader.SetPrompt(c.terminalPrompt.User)
return c.handleReadError(builder.Len()+len(input), err)
}
// write the response
c.io.Write(response.String())

if strings.HasSuffix(input, term) ||
strings.HasPrefix(input, handler.SystemCmdPrefix) {
builder.WriteString(strings.TrimSuffix(input, term))
if quit {
break
}

if builder.Len() == 0 {
c.reader.SetPrompt(c.terminalPrompt.UserNext)
}

builder.WriteString(input + "\n")
}
c.reader.SetPrompt(c.terminalPrompt.User)
return validateInput(builder.String())
}

func (c *Chat) handleReadError(inputLen int, err error) (string, bool) {
if errors.Is(err, readline.ErrInterrupt) {
if inputLen == 0 {
return handler.SystemCmdPrefix + handler.SystemCmdQuit, true
}
} else {
handler.PrintError(c.writer, c.terminalPrompt.Cli, err)
}
return "", false
}

func (c *Chat) getHandlerPrompt(message string) (handler.MessageHandler, string) {
if strings.HasPrefix(message, handler.SystemCmdPrefix) {
return c.systemHandler, c.terminalPrompt.Cli
// getHandler returns the handler for the message.
func (c *Chat) getHandler(prefix string) handler.MessageHandler {
if prefix == cli.SystemCmdPrefix {
return c.systemHandler
}
_, _ = fmt.Fprint(c.writer, c.terminalPrompt.Gemini)
return c.geminiHandler, ""
}

func validateInput(input string) (string, bool) {
input = strings.TrimSpace(input)
return input, input != ""
return c.geminiHandler
}
10 changes: 10 additions & 0 deletions internal/cli/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package cli

const (
SystemCmdPrefix = "!"
SystemCmdQuit = "q"
SystemCmdSelectPrompt = "p"
SystemCmdSelectInputMode = "i"
SystemCmdModel = "m"
SystemCmdHistory = "h"
)
13 changes: 6 additions & 7 deletions internal/handler/gemini_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,38 @@ import (

"github.com/charmbracelet/glamour"
"github.com/reugn/gemini-cli/gemini"
"github.com/reugn/gemini-cli/internal/cli"
)

// GeminiQuery processes queries to gemini models.
// It implements the MessageHandler interface.
type GeminiQuery struct {
*IO
session *gemini.ChatSession
spinner *cli.Spinner
renderer *glamour.TermRenderer
}

var _ MessageHandler = (*GeminiQuery)(nil)

// NewGeminiQuery returns a new GeminiQuery message handler.
func NewGeminiQuery(session *gemini.ChatSession, spinner *cli.Spinner,
style string) (*GeminiQuery, error) {
func NewGeminiQuery(io *IO, session *gemini.ChatSession, style string) (*GeminiQuery, error) {
renderer, err := glamour.NewTermRenderer(glamour.WithStylePath(style))
if err != nil {
return nil, fmt.Errorf("failed to instantiate terminal renderer: %w", err)
}

return &GeminiQuery{
IO: io,
session: session,
spinner: spinner,
renderer: renderer,
}, nil
}

// Handle processes the chat message.
func (h *GeminiQuery) Handle(message string) (Response, bool) {
h.spinner.Start()
h.terminal.Spinner.Start()
defer h.terminal.Spinner.Stop()

response, err := h.session.SendMessage(message)
h.spinner.Stop()
if err != nil {
return newErrorResponse(err), false
}
Expand Down
3 changes: 3 additions & 0 deletions internal/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ type MessageHandler interface {
// Handle processes the message and returns a response, along with a flag
// indicating whether the application should terminate.
Handle(message string) (Response, bool)

// TerminalPrompt returns the terminal prompt for the handler.
TerminalPrompt() string
}
8 changes: 7 additions & 1 deletion internal/handler/history_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@ var historyOptions = []string{
// HistoryCommand processes the chat history system commands.
// It implements the MessageHandler interface.
type HistoryCommand struct {
*IO
session *gemini.ChatSession
configuration *config.Configuration
}

var _ MessageHandler = (*HistoryCommand)(nil)

// NewHistoryCommand returns a new HistoryCommand.
func NewHistoryCommand(session *gemini.ChatSession,
func NewHistoryCommand(io *IO, session *gemini.ChatSession,
configuration *config.Configuration) *HistoryCommand {
return &HistoryCommand{
IO: io,
session: session,
configuration: configuration,
}
Expand Down Expand Up @@ -60,12 +62,14 @@ func (h *HistoryCommand) Handle(_ string) (Response, bool) {

// handleClear handles the chat history clear request.
func (h *HistoryCommand) handleClear() Response {
h.terminal.Write(h.terminalPrompt)
h.session.ClearHistory()
return dataResponse("Cleared the chat history.")
}

// handleStore handles the chat history store request.
func (h *HistoryCommand) handleStore() Response {
defer h.terminal.Write(h.terminalPrompt)
historyLabel, err := h.promptHistoryLabel()
if err != nil {
return newErrorResponse(err)
Expand All @@ -87,6 +91,7 @@ func (h *HistoryCommand) handleStore() Response {

// handleLoad handles the chat history load request.
func (h *HistoryCommand) handleLoad() Response {
defer h.terminal.Write(h.terminalPrompt)
label, history, err := h.loadHistory()
if err != nil {
return newErrorResponse(err)
Expand All @@ -98,6 +103,7 @@ func (h *HistoryCommand) handleLoad() Response {

// handleDelete handles deletion of the stored history records.
func (h *HistoryCommand) handleDelete() Response {
h.terminal.Write(h.terminalPrompt)
h.configuration.Data.History = make(map[string][]*gemini.SerializableContent)
if err := h.configuration.Flush(); err != nil {
return newErrorResponse(err)
Expand Down
24 changes: 11 additions & 13 deletions internal/handler/input_mode_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package handler
import (
"fmt"

"github.com/chzyer/readline"
"github.com/manifoldco/promptui"
)

Expand All @@ -15,42 +14,41 @@ var inputModeOptions = []string{
// InputModeCommand processes the chat input mode system command.
// It implements the MessageHandler interface.
type InputModeCommand struct {
reader *readline.Instance
multiline *bool
*IO
}

var _ MessageHandler = (*InputModeCommand)(nil)

// NewInputModeCommand returns a new InputModeCommand.
func NewInputModeCommand(reader *readline.Instance, multiline *bool) *InputModeCommand {
func NewInputModeCommand(io *IO) *InputModeCommand {
return &InputModeCommand{
reader: reader,
multiline: multiline,
IO: io,
}
}

// Handle processes the chat input mode system command.
func (h *InputModeCommand) Handle(_ string) (Response, bool) {
defer h.terminal.Write(h.terminalPrompt)
multiline, err := h.selectInputMode()
if err != nil {
return newErrorResponse(err), false
}

if *h.multiline == multiline {
if h.terminal.Config.Multiline == multiline {
// the same input mode is selected
return dataResponse(unchangedMessage), false
}

*h.multiline = multiline
if *h.multiline {
h.terminal.Config.Multiline = multiline
if h.terminal.Config.Multiline {
// disable history for multi-line messages since it is
// unusable for future requests
h.reader.HistoryDisable()
h.terminal.Reader.HistoryDisable()
} else {
h.reader.HistoryEnable()
h.terminal.Reader.HistoryEnable()
}

mode := inputModeOptions[modeIndex(*h.multiline)]
mode := inputModeOptions[modeIndex(h.terminal.Config.Multiline)]
return dataResponse(fmt.Sprintf("Switched to %q input mode.", mode)), false
}

Expand All @@ -61,7 +59,7 @@ func (h *InputModeCommand) selectInputMode() (bool, error) {
Label: "Select input mode",
HideSelected: true,
Items: inputModeOptions,
CursorPos: modeIndex(*h.multiline),
CursorPos: modeIndex(h.terminal.Config.Multiline),
}

_, result, err := prompt.Run()
Expand Down
Loading