feat(cli): support interactive start without initial prompt #1
5 changed files with 172 additions and 10 deletions
|
|
@ -86,6 +86,14 @@ This will:
|
||||||
|
|
||||||
Type `exit` or `quit` to end the conversation.
|
Type `exit` or `quit` to end the conversation.
|
||||||
|
|
||||||
|
You can also start without an initial prompt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
klod
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts the service and asks for your first prompt interactively.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Run without building:
|
Run without building:
|
||||||
|
|
|
||||||
7
go.mod
7
go.mod
|
|
@ -2,4 +2,9 @@ module klod
|
||||||
|
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|
||||||
require github.com/joho/godotenv v1.5.1
|
require (
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
golang.org/x/term v0.27.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.28.0 // indirect
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -1,2 +1,6 @@
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
|
|
|
||||||
145
main.go
145
main.go
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
|
|
@ -117,6 +118,133 @@ func logConversation(message Message) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enableBracketedPaste sends escape sequence to enable bracketed paste mode
|
||||||
|
func enableBracketedPaste() {
|
||||||
|
fmt.Print("\033[?2004h")
|
||||||
|
}
|
||||||
|
|
||||||
|
// disableBracketedPaste sends escape sequence to disable bracketed paste mode
|
||||||
|
func disableBracketedPaste() {
|
||||||
|
fmt.Print("\033[?2004l")
|
||||||
|
}
|
||||||
|
|
||||||
|
// readInputWithPasteSupport reads user input with support for multi-line paste
|
||||||
|
// It uses raw terminal mode to detect bracketed paste sequences
|
||||||
|
func readInputWithPasteSupport() (string, error) {
|
||||||
|
fd := int(os.Stdin.Fd())
|
||||||
|
|
||||||
|
// Check if stdin is a terminal
|
||||||
|
if !term.IsTerminal(fd) {
|
||||||
|
// Fall back to simple readline for non-terminal input
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
return strings.TrimSpace(line), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save terminal state and switch to raw mode
|
||||||
|
oldState, err := term.MakeRaw(fd)
|
||||||
|
if err != nil {
|
||||||
|
// Fall back to simple readline if raw mode fails
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
return strings.TrimSpace(line), err
|
||||||
|
}
|
||||||
|
defer term.Restore(fd, oldState)
|
||||||
|
|
||||||
|
var input strings.Builder
|
||||||
|
var buf [1]byte
|
||||||
|
inPaste := false
|
||||||
|
escBuf := make([]byte, 0, 10)
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, err := os.Stdin.Read(buf[:])
|
||||||
|
if err != nil || n == 0 {
|
||||||
|
return input.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
b := buf[0]
|
||||||
|
|
||||||
|
// Handle escape sequences
|
||||||
|
if b == 0x1b { // ESC
|
||||||
|
escBuf = append(escBuf[:0], b)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(escBuf) > 0 {
|
||||||
|
escBuf = append(escBuf, b)
|
||||||
|
|
||||||
|
// Check for bracketed paste start: ESC[200~
|
||||||
|
if len(escBuf) == 6 && escBuf[0] == 0x1b && escBuf[1] == '[' &&
|
||||||
|
escBuf[2] == '2' && escBuf[3] == '0' && escBuf[4] == '0' && escBuf[5] == '~' {
|
||||||
|
inPaste = true
|
||||||
|
escBuf = escBuf[:0]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for bracketed paste end: ESC[201~
|
||||||
|
if len(escBuf) == 6 && escBuf[0] == 0x1b && escBuf[1] == '[' &&
|
||||||
|
escBuf[2] == '2' && escBuf[3] == '0' && escBuf[4] == '1' && escBuf[5] == '~' {
|
||||||
|
inPaste = false
|
||||||
|
escBuf = escBuf[:0]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If sequence is getting too long without matching, it's not a paste sequence
|
||||||
|
// Write accumulated bytes and reset
|
||||||
|
if len(escBuf) > 6 {
|
||||||
|
for _, eb := range escBuf {
|
||||||
|
if eb >= 0x20 {
|
||||||
|
input.WriteByte(eb)
|
||||||
|
fmt.Print(string(eb))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
escBuf = escBuf[:0]
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle special keys
|
||||||
|
switch b {
|
||||||
|
case '\r', '\n': // Enter
|
||||||
|
if inPaste {
|
||||||
|
input.WriteByte('\n')
|
||||||
|
fmt.Print("\r\n") // Echo newline
|
||||||
|
} else {
|
||||||
|
fmt.Print("\r\n")
|
||||||
|
return input.String(), nil
|
||||||
|
}
|
||||||
|
case 0x03: // Ctrl+C
|
||||||
|
fmt.Print("^C\r\n")
|
||||||
|
return "", fmt.Errorf("interrupted")
|
||||||
|
case 0x04: // Ctrl+D
|
||||||
|
if input.Len() == 0 {
|
||||||
|
return "", io.EOF
|
||||||
|
}
|
||||||
|
return input.String(), nil
|
||||||
|
case 0x7f, 0x08: // Backspace/Delete
|
||||||
|
s := input.String()
|
||||||
|
if len(s) > 0 {
|
||||||
|
// Handle multi-byte UTF-8 characters
|
||||||
|
runes := []rune(s)
|
||||||
|
if len(runes) > 0 {
|
||||||
|
input.Reset()
|
||||||
|
input.WriteString(string(runes[:len(runes)-1]))
|
||||||
|
fmt.Print("\b \b") // Erase character on screen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if b >= 0x20 || b == '\t' { // Printable character or tab
|
||||||
|
input.WriteByte(b)
|
||||||
|
if b == '\t' {
|
||||||
|
fmt.Print(" ") // Display tab as spaces
|
||||||
|
} else {
|
||||||
|
fmt.Print(string(b)) // Echo character
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func loadConfig() error {
|
func loadConfig() error {
|
||||||
// Try loading config from multiple locations in order
|
// Try loading config from multiple locations in order
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
|
|
@ -209,23 +337,32 @@ func main() {
|
||||||
logConversation(assistantMsg)
|
logConversation(assistantMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interactive conversation loop
|
// Interactive conversation loop with bracketed paste support
|
||||||
reader := bufio.NewReader(os.Stdin)
|
enableBracketedPaste()
|
||||||
|
defer disableBracketedPaste()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
fmt.Print("\033[32mYou (or 'exit' to quit): \033[0m")
|
fmt.Print("\033[32mYou (or 'exit' to quit): \033[0m")
|
||||||
|
|
||||||
userInput, err := reader.ReadString('\n')
|
userInput, err := readInputWithPasteSupport()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
fmt.Println("\nGoodbye!")
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
userInput = strings.TrimSpace(userInput)
|
userInput = strings.TrimSpace(userInput)
|
||||||
|
|
||||||
// Check if user wants to exit
|
// Check if user wants to exit
|
||||||
if userInput == "exit" || userInput == "quit" || userInput == "" {
|
if userInput == "exit" || userInput == "quit" {
|
||||||
fmt.Println("Goodbye!")
|
fmt.Println("Goodbye!")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if userInput == "" {
|
||||||
|
fmt.Println("Please enter a prompt (or type 'exit' to quit).")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Add user input to conversation history
|
// Add user input to conversation history
|
||||||
userMsg := Message{
|
userMsg := Message{
|
||||||
|
|
|
||||||
10
main.sh
10
main.sh
|
|
@ -78,11 +78,15 @@ send_message() {
|
||||||
CONVERSATION_HISTORY+=("$response")
|
CONVERSATION_HISTORY+=("$response")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if [[ -n "$PROMPT" ]]; then
|
||||||
# Add initial user prompt to conversation history
|
# Add initial user prompt to conversation history
|
||||||
CONVERSATION_HISTORY+=("$PROMPT")
|
CONVERSATION_HISTORY+=("$PROMPT")
|
||||||
|
|
||||||
# Send initial message
|
# Send initial message
|
||||||
send_message
|
send_message
|
||||||
|
else
|
||||||
|
:
|
||||||
|
fi
|
||||||
|
|
||||||
# Conversation loop
|
# Conversation loop
|
||||||
while true; do
|
while true; do
|
||||||
|
|
@ -91,10 +95,14 @@ while true; do
|
||||||
read -e user_input
|
read -e user_input
|
||||||
|
|
||||||
# Check if user wants to exit
|
# Check if user wants to exit
|
||||||
if [[ "$user_input" == "exit" ]] || [[ "$user_input" == "quit" ]] || [[ -z "$user_input" ]]; then
|
if [[ "$user_input" == "exit" ]] || [[ "$user_input" == "quit" ]]; then
|
||||||
echo "Goodbye!"
|
echo "Goodbye!"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
if [[ -z "$user_input" ]]; then
|
||||||
|
echo "Please enter a prompt (or type 'exit' to quit)."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
# Add user input to conversation history
|
# Add user input to conversation history
|
||||||
CONVERSATION_HISTORY+=("$user_input")
|
CONVERSATION_HISTORY+=("$user_input")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue