Skip to content

Commit

Permalink
refactor: internal packages structure
Browse files Browse the repository at this point in the history
  • Loading branch information
reugn committed Dec 5, 2024
1 parent 9cedafb commit e9613af
Show file tree
Hide file tree
Showing 18 changed files with 265 additions and 175 deletions.
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

0 comments on commit e9613af

Please sign in to comment.