Feat: shell mode
This commit is contained in:
11
helpfuncs.go
11
helpfuncs.go
@@ -209,8 +209,17 @@ func makeStatusLine() string {
|
||||
} else {
|
||||
imageInfo = ""
|
||||
}
|
||||
|
||||
// Add shell mode status to status line
|
||||
var shellModeInfo string
|
||||
if shellMode {
|
||||
shellModeInfo = " | [green:-:b]SHELL MODE[-:-:-]"
|
||||
} else {
|
||||
shellModeInfo = ""
|
||||
}
|
||||
|
||||
statusLine := fmt.Sprintf(indexLineCompletion, botRespMode, cfg.AssistantRole, activeChatName,
|
||||
cfg.ToolUse, chatBody.Model, cfg.SkipLLMResp, cfg.CurrentAPI, cfg.ThinkUse, logLevel.Level(),
|
||||
isRecording, persona, botPersona, injectRole)
|
||||
return statusLine + imageInfo
|
||||
return statusLine + imageInfo + shellModeInfo
|
||||
}
|
||||
|
||||
1
main.go
1
main.go
@@ -15,6 +15,7 @@ var (
|
||||
selectedIndex = int(-1)
|
||||
currentAPIIndex = 0 // Index to track current API in ApiLinks slice
|
||||
currentORModelIndex = 0 // Index to track current OpenRouter model in ORFreeModels slice
|
||||
shellMode = false
|
||||
// indexLine = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | card's char: [orange:-:b]%s[-:-:-] (ctrl+s) | chat: [orange:-:b]%s[-:-:-] (F1) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [orange:-:b]%v[-:-:-] (F10)\nAPI_URL: [orange:-:b]%s[-:-:-] (ctrl+v) | ThinkUse: [orange:-:b]%v[-:-:-] (ctrl+p) | Log Level: [orange:-:b]%v[-:-:-] (ctrl+p) | Recording: [orange:-:b]%v[-:-:-] (ctrl+r) | Writing as: [orange:-:b]%s[-:-:-] (ctrl+q)"
|
||||
indexLineCompletion = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | card's char: [orange:-:b]%s[-:-:-] (ctrl+s) | chat: [orange:-:b]%s[-:-:-] (F1) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [orange:-:b]%v[-:-:-] (F10)\nAPI_URL: [orange:-:b]%s[-:-:-] (ctrl+v) | Insert <think>: [orange:-:b]%v[-:-:-] (ctrl+p) | Log Level: [orange:-:b]%v[-:-:-] (ctrl+p) | Recording: [orange:-:b]%v[-:-:-] (ctrl+r) | Writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | Bot will write as [orange:-:b]%s[-:-:-] (ctrl+x) | role_inject [orange:-:b]%v[-:-:-]"
|
||||
focusSwitcher = map[tview.Primitive]tview.Primitive{}
|
||||
|
||||
113
tui.go
113
tui.go
@@ -8,6 +8,7 @@ import (
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -78,6 +79,7 @@ var (
|
||||
[yellow]Ctrl+y[white]: list loaded RAG files (view and manage loaded files)
|
||||
[yellow]Ctrl+q[white]: cycle through mentioned chars in chat, to pick persona to send next msg as
|
||||
[yellow]Ctrl+x[white]: cycle through mentioned chars in chat, to pick persona to send next msg as (for llm)
|
||||
[yellow]Alt+1[white]: toggle shell mode (execute commands locally)
|
||||
|
||||
=== scrolling chat window (some keys similar to vim) ===
|
||||
[yellow]arrows up/down and j/k[white]: scroll up and down
|
||||
@@ -207,6 +209,102 @@ func makePropsForm(props map[string]float32) *tview.Form {
|
||||
return form
|
||||
}
|
||||
|
||||
func toggleShellMode() {
|
||||
shellMode = !shellMode
|
||||
if shellMode {
|
||||
// Update input placeholder to indicate shell mode
|
||||
textArea.SetPlaceholder("SHELL MODE: Enter command and press <Esc> to execute")
|
||||
} else {
|
||||
// Reset to normal mode
|
||||
textArea.SetPlaceholder("input is multiline; press <Enter> to start the next line;\npress <Esc> to send the message. Alt+1 to exit shell mode")
|
||||
}
|
||||
updateStatusLine()
|
||||
}
|
||||
|
||||
func executeCommandAndDisplay(cmdText string) {
|
||||
// Parse the command (split by spaces, but handle quoted arguments)
|
||||
cmdParts := parseCommand(cmdText)
|
||||
if len(cmdParts) == 0 {
|
||||
fmt.Fprintf(textView, "\n[red]Error: No command provided[-:-:-]\n")
|
||||
textView.ScrollToEnd()
|
||||
colorText()
|
||||
return
|
||||
}
|
||||
|
||||
command := cmdParts[0]
|
||||
args := []string{}
|
||||
if len(cmdParts) > 1 {
|
||||
args = cmdParts[1:]
|
||||
}
|
||||
|
||||
// Create the command execution
|
||||
cmd := exec.Command(command, args...)
|
||||
|
||||
// Execute the command and get output
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
// Add the command being executed to the chat
|
||||
fmt.Fprintf(textView, "\n[yellow]$ %s[-:-:-]\n", cmdText)
|
||||
|
||||
if err != nil {
|
||||
// Include both output and error
|
||||
fmt.Fprintf(textView, "[red]Error: %s[-:-:-]\n", err.Error())
|
||||
if len(output) > 0 {
|
||||
fmt.Fprintf(textView, "[red]%s[-:-:-]\n", string(output))
|
||||
}
|
||||
} else {
|
||||
// Only output if successful
|
||||
if len(output) > 0 {
|
||||
fmt.Fprintf(textView, "[green]%s[-:-:-]\n", string(output))
|
||||
} else {
|
||||
fmt.Fprintf(textView, "[green]Command executed successfully (no output)[-:-:-]\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to end and update colors
|
||||
textView.ScrollToEnd()
|
||||
colorText()
|
||||
}
|
||||
|
||||
// parseCommand splits command string handling quotes properly
|
||||
func parseCommand(cmd string) []string {
|
||||
var args []string
|
||||
var current string
|
||||
var inQuotes bool
|
||||
var quoteChar rune
|
||||
|
||||
for _, r := range cmd {
|
||||
switch r {
|
||||
case '"', '\'':
|
||||
if inQuotes {
|
||||
if r == quoteChar {
|
||||
inQuotes = false
|
||||
} else {
|
||||
current += string(r)
|
||||
}
|
||||
} else {
|
||||
inQuotes = true
|
||||
quoteChar = r
|
||||
}
|
||||
case ' ', '\t':
|
||||
if inQuotes {
|
||||
current += string(r)
|
||||
} else if current != "" {
|
||||
args = append(args, current)
|
||||
current = ""
|
||||
}
|
||||
default:
|
||||
current += string(r)
|
||||
}
|
||||
}
|
||||
|
||||
if current != "" {
|
||||
args = append(args, current)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func init() {
|
||||
tview.Styles = colorschemes["default"]
|
||||
app = tview.NewApplication()
|
||||
@@ -800,10 +898,22 @@ func init() {
|
||||
pages.AddPage(RAGLoadedPage, chatLoadedRAGTable, true, true)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyRune && event.Modifiers() == tcell.ModAlt && event.Rune() == '1' {
|
||||
// Toggle shell mode: when enabled, commands are executed locally instead of sent to LLM
|
||||
toggleShellMode()
|
||||
return nil
|
||||
}
|
||||
// cannot send msg in editMode or botRespMode
|
||||
if event.Key() == tcell.KeyEscape && !editMode && !botRespMode {
|
||||
// read all text into buffer
|
||||
msgText := textArea.GetText()
|
||||
|
||||
if shellMode && msgText != "" {
|
||||
// In shell mode, execute command instead of sending to LLM
|
||||
executeCommandAndDisplay(msgText)
|
||||
textArea.SetText("", true) // Clear the input area
|
||||
return nil
|
||||
} else if !shellMode {
|
||||
// Normal mode - send to LLM
|
||||
nl := "\n"
|
||||
prevText := textView.GetText(true)
|
||||
persona := cfg.UserRole
|
||||
@@ -840,6 +950,7 @@ func init() {
|
||||
// But clears it for the next message
|
||||
ClearImageAttachment()
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyPgUp || event.Key() == tcell.KeyPgDn {
|
||||
|
||||
Reference in New Issue
Block a user