Compare commits
3 Commits
7d51c5d0f3
...
0d94734090
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d94734090 | ||
|
|
a0ff384b81 | ||
|
|
09b5e0d08f |
29
helpfuncs.go
29
helpfuncs.go
@@ -426,12 +426,11 @@ func deepseekModelValidator() error {
|
|||||||
|
|
||||||
func toggleShellMode() {
|
func toggleShellMode() {
|
||||||
shellMode = !shellMode
|
shellMode = !shellMode
|
||||||
|
setShellMode(shellMode)
|
||||||
if shellMode {
|
if shellMode {
|
||||||
// Update input placeholder to indicate shell mode
|
shellInput.SetLabel(fmt.Sprintf("[%s]$ ", cfg.FilePickerDir))
|
||||||
textArea.SetPlaceholder("SHELL MODE: Enter command and press <Esc> to execute")
|
|
||||||
} else {
|
} else {
|
||||||
// Reset to normal mode
|
textArea.SetPlaceholder("input is multiline; press <Enter> to start the next line;\npress <Esc> to send the message.")
|
||||||
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()
|
updateStatusLine()
|
||||||
}
|
}
|
||||||
@@ -443,15 +442,22 @@ func updateFlexLayout() {
|
|||||||
}
|
}
|
||||||
flex.Clear()
|
flex.Clear()
|
||||||
flex.AddItem(textView, 0, 40, false)
|
flex.AddItem(textView, 0, 40, false)
|
||||||
flex.AddItem(textArea, 0, 10, false)
|
if shellMode {
|
||||||
|
flex.AddItem(shellInput, 0, 10, false)
|
||||||
|
} else {
|
||||||
|
flex.AddItem(textArea, 0, 10, false)
|
||||||
|
}
|
||||||
if positionVisible {
|
if positionVisible {
|
||||||
flex.AddItem(statusLineWidget, 0, 2, false)
|
flex.AddItem(statusLineWidget, 0, 2, false)
|
||||||
}
|
}
|
||||||
// Keep focus on currently focused widget
|
// Keep focus on currently focused widget
|
||||||
focused := app.GetFocus()
|
focused := app.GetFocus()
|
||||||
if focused == textView {
|
switch {
|
||||||
|
case focused == textView:
|
||||||
app.SetFocus(textView)
|
app.SetFocus(textView)
|
||||||
} else {
|
case shellMode:
|
||||||
|
app.SetFocus(shellInput)
|
||||||
|
default:
|
||||||
app.SetFocus(textArea)
|
app.SetFocus(textArea)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,10 +480,12 @@ func executeCommandAndDisplay(cmdText string) {
|
|||||||
}
|
}
|
||||||
// Create the command execution
|
// Create the command execution
|
||||||
cmd := exec.Command(command, args...)
|
cmd := exec.Command(command, args...)
|
||||||
|
cmd.Dir = cfg.FilePickerDir
|
||||||
// Execute the command and get output
|
// Execute the command and get output
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
// Add the command being executed to the chat
|
// Add the command being executed to the chat
|
||||||
fmt.Fprintf(textView, "\n[yellow]$ %s[-:-:-]\n", cmdText)
|
fmt.Fprintf(textView, "\n[-:-:b](%d) <%s>: [-:-:-]\n$ %s\n",
|
||||||
|
len(chatBody.Messages), cfg.ToolRole, cmdText)
|
||||||
var outputContent string
|
var outputContent string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Include both output and error
|
// Include both output and error
|
||||||
@@ -514,6 +522,11 @@ func executeCommandAndDisplay(cmdText string) {
|
|||||||
textView.ScrollToEnd()
|
textView.ScrollToEnd()
|
||||||
}
|
}
|
||||||
colorText()
|
colorText()
|
||||||
|
// Add command to history (avoid duplicates at the end)
|
||||||
|
if len(shellHistory) == 0 || shellHistory[len(shellHistory)-1] != cmdText {
|
||||||
|
shellHistory = append(shellHistory, cmdText)
|
||||||
|
}
|
||||||
|
shellHistoryPos = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseCommand splits command string handling quotes properly
|
// parseCommand splits command string handling quotes properly
|
||||||
|
|||||||
8
main.go
8
main.go
@@ -13,9 +13,11 @@ var (
|
|||||||
injectRole = true
|
injectRole = true
|
||||||
selectedIndex = int(-1)
|
selectedIndex = int(-1)
|
||||||
shellMode = false
|
shellMode = false
|
||||||
thinkingCollapsed = false
|
shellHistory []string
|
||||||
statusLineTempl = "help (F12) | [%s:-:b]llm writes[-:-:-] (F6 to interrupt) | chat: [orange:-:b]%s[-:-:-] (F1) | [%s:-:b]tool use[-:-:-] (ctrl+k) | model: [%s:-:b]%s[-:-:-] (ctrl+l) | [%s:-:b]skip LLM resp[-:-:-] (F10)\nAPI: [orange:-:b]%s[-:-:-] (ctrl+v) | writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | bot will write as [orange:-:b]%s[-:-:-] (ctrl+x)"
|
shellHistoryPos int = -1
|
||||||
focusSwitcher = map[tview.Primitive]tview.Primitive{}
|
thinkingCollapsed = false
|
||||||
|
statusLineTempl = "help (F12) | [%s:-:b]llm writes[-:-:-] (F6 to interrupt) | chat: [orange:-:b]%s[-:-:-] (F1) | [%s:-:b]tool use[-:-:-] (ctrl+k) | model: [%s:-:b]%s[-:-:-] (ctrl+l) | [%s:-:b]skip LLM resp[-:-:-] (F10)\nAPI: [orange:-:b]%s[-:-:-] (ctrl+v) | writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | bot will write as [orange:-:b]%s[-:-:-] (ctrl+x)"
|
||||||
|
focusSwitcher = map[tview.Primitive]tview.Primitive{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
26
popups.go
26
popups.go
@@ -343,7 +343,7 @@ func showBotRoleSelectionPopup() {
|
|||||||
app.SetFocus(roleListWidget)
|
app.SetFocus(roleListWidget)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showFileCompletionPopup(filter string) {
|
func showShellFileCompletionPopup(filter string) {
|
||||||
baseDir := cfg.FilePickerDir
|
baseDir := cfg.FilePickerDir
|
||||||
if baseDir == "" {
|
if baseDir == "" {
|
||||||
baseDir = "."
|
baseDir = "."
|
||||||
@@ -352,13 +352,12 @@ func showFileCompletionPopup(filter string) {
|
|||||||
if len(complMatches) == 0 {
|
if len(complMatches) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// If only one match, auto-complete without showing popup
|
|
||||||
if len(complMatches) == 1 {
|
if len(complMatches) == 1 {
|
||||||
currentText := textArea.GetText()
|
currentText := shellInput.GetText()
|
||||||
atIdx := strings.LastIndex(currentText, "@")
|
atIdx := strings.LastIndex(currentText, "@")
|
||||||
if atIdx >= 0 {
|
if atIdx >= 0 {
|
||||||
before := currentText[:atIdx]
|
before := currentText[:atIdx]
|
||||||
textArea.SetText(before+complMatches[0], true)
|
shellInput.SetText(before + complMatches[0])
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -369,24 +368,24 @@ func showFileCompletionPopup(filter string) {
|
|||||||
widget.AddItem(m, "", 0, nil)
|
widget.AddItem(m, "", 0, nil)
|
||||||
}
|
}
|
||||||
widget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
widget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
||||||
currentText := textArea.GetText()
|
currentText := shellInput.GetText()
|
||||||
atIdx := strings.LastIndex(currentText, "@")
|
atIdx := strings.LastIndex(currentText, "@")
|
||||||
if atIdx >= 0 {
|
if atIdx >= 0 {
|
||||||
before := currentText[:atIdx]
|
before := currentText[:atIdx]
|
||||||
textArea.SetText(before+mainText, true)
|
shellInput.SetText(before + mainText)
|
||||||
}
|
}
|
||||||
pages.RemovePage("fileCompletionPopup")
|
pages.RemovePage("shellFileCompletionPopup")
|
||||||
app.SetFocus(textArea)
|
app.SetFocus(shellInput)
|
||||||
})
|
})
|
||||||
widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if event.Key() == tcell.KeyEscape {
|
if event.Key() == tcell.KeyEscape {
|
||||||
pages.RemovePage("fileCompletionPopup")
|
pages.RemovePage("shellFileCompletionPopup")
|
||||||
app.SetFocus(textArea)
|
app.SetFocus(shellInput)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||||
pages.RemovePage("fileCompletionPopup")
|
pages.RemovePage("shellFileCompletionPopup")
|
||||||
app.SetFocus(textArea)
|
app.SetFocus(shellInput)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return event
|
return event
|
||||||
@@ -400,8 +399,7 @@ func showFileCompletionPopup(filter string) {
|
|||||||
AddItem(nil, 0, 1, false), width, 1, true).
|
AddItem(nil, 0, 1, false), width, 1, true).
|
||||||
AddItem(nil, 0, 1, false)
|
AddItem(nil, 0, 1, false)
|
||||||
}
|
}
|
||||||
// Add modal page and make it visible
|
pages.AddPage("shellFileCompletionPopup", modal(widget, 80, 20), true, true)
|
||||||
pages.AddPage("fileCompletionPopup", modal(widget, 80, 20), true, true)
|
|
||||||
app.SetFocus(widget)
|
app.SetFocus(widget)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
tools.go
1
tools.go
@@ -714,6 +714,7 @@ func executeCommand(args map[string]string) []byte {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
cmd := exec.CommandContext(ctx, command, cmdArgs...)
|
cmd := exec.CommandContext(ctx, command, cmdArgs...)
|
||||||
|
cmd.Dir = cfg.FilePickerDir
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("command '%s' failed; error: %v; output: %s", command, err, string(output))
|
msg := fmt.Sprintf("command '%s' failed; error: %v; output: %s", command, err, string(output))
|
||||||
|
|||||||
101
tui.go
101
tui.go
@@ -34,6 +34,7 @@ var (
|
|||||||
indexPickWindow *tview.InputField
|
indexPickWindow *tview.InputField
|
||||||
renameWindow *tview.InputField
|
renameWindow *tview.InputField
|
||||||
roleEditWindow *tview.InputField
|
roleEditWindow *tview.InputField
|
||||||
|
shellInput *tview.InputField
|
||||||
fullscreenMode bool
|
fullscreenMode bool
|
||||||
positionVisible bool = true
|
positionVisible bool = true
|
||||||
scrollToEndEnabled bool = true
|
scrollToEndEnabled bool = true
|
||||||
@@ -124,46 +125,78 @@ Press <Enter> or 'x' to return
|
|||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func setShellMode(enabled bool) {
|
||||||
|
shellMode = enabled
|
||||||
|
go func() {
|
||||||
|
app.QueueUpdateDraw(func() {
|
||||||
|
updateFlexLayout()
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Start background goroutine to update model color cache
|
// Start background goroutine to update model color cache
|
||||||
startModelColorUpdater()
|
startModelColorUpdater()
|
||||||
tview.Styles = colorschemes["default"]
|
tview.Styles = colorschemes["default"]
|
||||||
app = tview.NewApplication()
|
app = tview.NewApplication()
|
||||||
pages = tview.NewPages()
|
pages = tview.NewPages()
|
||||||
textArea = tview.NewTextArea().
|
shellInput = tview.NewInputField().
|
||||||
SetPlaceholder("input is multiline; press <Enter> to start the next line;\npress <Esc> to send the message.")
|
SetLabel(fmt.Sprintf("[%s]$ ", cfg.FilePickerDir)). // dynamic prompt
|
||||||
textArea.SetBorder(true).SetTitle("input")
|
SetFieldWidth(0).
|
||||||
// Add input capture for @ completion
|
SetDoneFunc(func(key tcell.Key) {
|
||||||
textArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
if key == tcell.KeyEnter {
|
||||||
|
cmd := shellInput.GetText()
|
||||||
|
if cmd != "" {
|
||||||
|
executeCommandAndDisplay(cmd)
|
||||||
|
}
|
||||||
|
shellInput.SetText("")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Copy your file completion logic to shellInput's InputCapture
|
||||||
|
shellInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if !shellMode {
|
if !shellMode {
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
// Handle Tab key for file completion
|
// Handle Up arrow for history previous
|
||||||
if event.Key() == tcell.KeyTab {
|
if event.Key() == tcell.KeyUp {
|
||||||
currentText := textArea.GetText()
|
if len(shellHistory) > 0 {
|
||||||
row, col, _, _ := textArea.GetCursor()
|
if shellHistoryPos < len(shellHistory)-1 {
|
||||||
// Calculate absolute position from row/col
|
shellHistoryPos++
|
||||||
lines := strings.Split(currentText, "\n")
|
shellInput.SetText(shellHistory[len(shellHistory)-1-shellHistoryPos])
|
||||||
cursorPos := 0
|
|
||||||
for i := 0; i < row && i < len(lines); i++ {
|
|
||||||
cursorPos += len(lines[i]) + 1 // +1 for newline
|
|
||||||
}
|
|
||||||
cursorPos += col
|
|
||||||
// Look backwards from cursor to find @
|
|
||||||
if cursorPos > 0 {
|
|
||||||
// Find the last @ before cursor
|
|
||||||
textBeforeCursor := currentText[:cursorPos]
|
|
||||||
atIndex := strings.LastIndex(textBeforeCursor, "@")
|
|
||||||
if atIndex >= 0 {
|
|
||||||
// Extract the partial match text after @
|
|
||||||
filter := textBeforeCursor[atIndex+1:]
|
|
||||||
showFileCompletionPopup(filter)
|
|
||||||
return nil // Consume the Tab event
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Handle Down arrow for history next
|
||||||
|
if event.Key() == tcell.KeyDown {
|
||||||
|
if shellHistoryPos > 0 {
|
||||||
|
shellHistoryPos--
|
||||||
|
shellInput.SetText(shellHistory[len(shellHistory)-1-shellHistoryPos])
|
||||||
|
} else if shellHistoryPos == 0 {
|
||||||
|
shellHistoryPos = -1
|
||||||
|
shellInput.SetText("")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Reset history position when user types
|
||||||
|
if event.Key() == tcell.KeyRune {
|
||||||
|
shellHistoryPos = -1
|
||||||
|
}
|
||||||
|
// Handle Tab key for @ file completion
|
||||||
|
if event.Key() == tcell.KeyTab {
|
||||||
|
currentText := shellInput.GetText()
|
||||||
|
atIndex := strings.LastIndex(currentText, "@")
|
||||||
|
if atIndex >= 0 {
|
||||||
|
filter := currentText[atIndex+1:]
|
||||||
|
showShellFileCompletionPopup(filter)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return event
|
return event
|
||||||
})
|
})
|
||||||
|
textArea = tview.NewTextArea().
|
||||||
|
SetPlaceholder("input is multiline; press <Enter> to start the next line;\npress <Esc> to send the message.")
|
||||||
|
textArea.SetBorder(true).SetTitle("input")
|
||||||
textView = tview.NewTextView().
|
textView = tview.NewTextView().
|
||||||
SetDynamicColors(true).
|
SetDynamicColors(true).
|
||||||
SetRegions(true).
|
SetRegions(true).
|
||||||
@@ -948,14 +981,16 @@ func init() {
|
|||||||
}
|
}
|
||||||
// cannot send msg in editMode or botRespMode
|
// cannot send msg in editMode or botRespMode
|
||||||
if event.Key() == tcell.KeyEscape && !editMode && !botRespMode {
|
if event.Key() == tcell.KeyEscape && !editMode && !botRespMode {
|
||||||
msgText := textArea.GetText()
|
if shellMode {
|
||||||
if shellMode && msgText != "" {
|
cmdText := shellInput.GetText()
|
||||||
// In shell mode, execute command instead of sending to LLM
|
if cmdText != "" {
|
||||||
executeCommandAndDisplay(msgText)
|
executeCommandAndDisplay(cmdText)
|
||||||
textArea.SetText("", true) // Clear the input area
|
shellInput.SetText("")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
} else if !shellMode {
|
}
|
||||||
// Normal mode - send to LLM
|
msgText := textArea.GetText()
|
||||||
|
if msgText != "" {
|
||||||
nl := "\n\n" // keep empty lines between messages
|
nl := "\n\n" // keep empty lines between messages
|
||||||
prevText := textView.GetText(true)
|
prevText := textView.GetText(true)
|
||||||
persona := cfg.UserRole
|
persona := cfg.UserRole
|
||||||
|
|||||||
Reference in New Issue
Block a user