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() {
|
||||
shellMode = !shellMode
|
||||
setShellMode(shellMode)
|
||||
if shellMode {
|
||||
// Update input placeholder to indicate shell mode
|
||||
textArea.SetPlaceholder("SHELL MODE: Enter command and press <Esc> to execute")
|
||||
shellInput.SetLabel(fmt.Sprintf("[%s]$ ", cfg.FilePickerDir))
|
||||
} 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")
|
||||
textArea.SetPlaceholder("input is multiline; press <Enter> to start the next line;\npress <Esc> to send the message.")
|
||||
}
|
||||
updateStatusLine()
|
||||
}
|
||||
@@ -443,15 +442,22 @@ func updateFlexLayout() {
|
||||
}
|
||||
flex.Clear()
|
||||
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 {
|
||||
flex.AddItem(statusLineWidget, 0, 2, false)
|
||||
}
|
||||
// Keep focus on currently focused widget
|
||||
focused := app.GetFocus()
|
||||
if focused == textView {
|
||||
switch {
|
||||
case focused == textView:
|
||||
app.SetFocus(textView)
|
||||
} else {
|
||||
case shellMode:
|
||||
app.SetFocus(shellInput)
|
||||
default:
|
||||
app.SetFocus(textArea)
|
||||
}
|
||||
}
|
||||
@@ -474,10 +480,12 @@ func executeCommandAndDisplay(cmdText string) {
|
||||
}
|
||||
// Create the command execution
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Dir = cfg.FilePickerDir
|
||||
// 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)
|
||||
fmt.Fprintf(textView, "\n[-:-:b](%d) <%s>: [-:-:-]\n$ %s\n",
|
||||
len(chatBody.Messages), cfg.ToolRole, cmdText)
|
||||
var outputContent string
|
||||
if err != nil {
|
||||
// Include both output and error
|
||||
@@ -514,6 +522,11 @@ func executeCommandAndDisplay(cmdText string) {
|
||||
textView.ScrollToEnd()
|
||||
}
|
||||
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
|
||||
|
||||
8
main.go
8
main.go
@@ -13,9 +13,11 @@ var (
|
||||
injectRole = true
|
||||
selectedIndex = int(-1)
|
||||
shellMode = false
|
||||
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{}
|
||||
shellHistory []string
|
||||
shellHistoryPos int = -1
|
||||
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() {
|
||||
|
||||
26
popups.go
26
popups.go
@@ -343,7 +343,7 @@ func showBotRoleSelectionPopup() {
|
||||
app.SetFocus(roleListWidget)
|
||||
}
|
||||
|
||||
func showFileCompletionPopup(filter string) {
|
||||
func showShellFileCompletionPopup(filter string) {
|
||||
baseDir := cfg.FilePickerDir
|
||||
if baseDir == "" {
|
||||
baseDir = "."
|
||||
@@ -352,13 +352,12 @@ func showFileCompletionPopup(filter string) {
|
||||
if len(complMatches) == 0 {
|
||||
return
|
||||
}
|
||||
// If only one match, auto-complete without showing popup
|
||||
if len(complMatches) == 1 {
|
||||
currentText := textArea.GetText()
|
||||
currentText := shellInput.GetText()
|
||||
atIdx := strings.LastIndex(currentText, "@")
|
||||
if atIdx >= 0 {
|
||||
before := currentText[:atIdx]
|
||||
textArea.SetText(before+complMatches[0], true)
|
||||
shellInput.SetText(before + complMatches[0])
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -369,24 +368,24 @@ func showFileCompletionPopup(filter string) {
|
||||
widget.AddItem(m, "", 0, nil)
|
||||
}
|
||||
widget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
|
||||
currentText := textArea.GetText()
|
||||
currentText := shellInput.GetText()
|
||||
atIdx := strings.LastIndex(currentText, "@")
|
||||
if atIdx >= 0 {
|
||||
before := currentText[:atIdx]
|
||||
textArea.SetText(before+mainText, true)
|
||||
shellInput.SetText(before + mainText)
|
||||
}
|
||||
pages.RemovePage("fileCompletionPopup")
|
||||
app.SetFocus(textArea)
|
||||
pages.RemovePage("shellFileCompletionPopup")
|
||||
app.SetFocus(shellInput)
|
||||
})
|
||||
widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
pages.RemovePage("fileCompletionPopup")
|
||||
app.SetFocus(textArea)
|
||||
pages.RemovePage("shellFileCompletionPopup")
|
||||
app.SetFocus(shellInput)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||
pages.RemovePage("fileCompletionPopup")
|
||||
app.SetFocus(textArea)
|
||||
pages.RemovePage("shellFileCompletionPopup")
|
||||
app.SetFocus(shellInput)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
@@ -400,8 +399,7 @@ func showFileCompletionPopup(filter string) {
|
||||
AddItem(nil, 0, 1, false), width, 1, true).
|
||||
AddItem(nil, 0, 1, false)
|
||||
}
|
||||
// Add modal page and make it visible
|
||||
pages.AddPage("fileCompletionPopup", modal(widget, 80, 20), true, true)
|
||||
pages.AddPage("shellFileCompletionPopup", modal(widget, 80, 20), true, true)
|
||||
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)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, command, cmdArgs...)
|
||||
cmd.Dir = cfg.FilePickerDir
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
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
|
||||
renameWindow *tview.InputField
|
||||
roleEditWindow *tview.InputField
|
||||
shellInput *tview.InputField
|
||||
fullscreenMode bool
|
||||
positionVisible 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() {
|
||||
// Start background goroutine to update model color cache
|
||||
startModelColorUpdater()
|
||||
tview.Styles = colorschemes["default"]
|
||||
app = tview.NewApplication()
|
||||
pages = tview.NewPages()
|
||||
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")
|
||||
// Add input capture for @ completion
|
||||
textArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
shellInput = tview.NewInputField().
|
||||
SetLabel(fmt.Sprintf("[%s]$ ", cfg.FilePickerDir)). // dynamic prompt
|
||||
SetFieldWidth(0).
|
||||
SetDoneFunc(func(key tcell.Key) {
|
||||
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 {
|
||||
return event
|
||||
}
|
||||
// Handle Tab key for file completion
|
||||
if event.Key() == tcell.KeyTab {
|
||||
currentText := textArea.GetText()
|
||||
row, col, _, _ := textArea.GetCursor()
|
||||
// Calculate absolute position from row/col
|
||||
lines := strings.Split(currentText, "\n")
|
||||
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
|
||||
// Handle Up arrow for history previous
|
||||
if event.Key() == tcell.KeyUp {
|
||||
if len(shellHistory) > 0 {
|
||||
if shellHistoryPos < len(shellHistory)-1 {
|
||||
shellHistoryPos++
|
||||
shellInput.SetText(shellHistory[len(shellHistory)-1-shellHistoryPos])
|
||||
}
|
||||
}
|
||||
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
|
||||
})
|
||||
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().
|
||||
SetDynamicColors(true).
|
||||
SetRegions(true).
|
||||
@@ -948,14 +981,16 @@ func init() {
|
||||
}
|
||||
// cannot send msg in editMode or botRespMode
|
||||
if event.Key() == tcell.KeyEscape && !editMode && !botRespMode {
|
||||
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
|
||||
if shellMode {
|
||||
cmdText := shellInput.GetText()
|
||||
if cmdText != "" {
|
||||
executeCommandAndDisplay(cmdText)
|
||||
shellInput.SetText("")
|
||||
}
|
||||
return nil
|
||||
} else if !shellMode {
|
||||
// Normal mode - send to LLM
|
||||
}
|
||||
msgText := textArea.GetText()
|
||||
if msgText != "" {
|
||||
nl := "\n\n" // keep empty lines between messages
|
||||
prevText := textView.GetText(true)
|
||||
persona := cfg.UserRole
|
||||
|
||||
Reference in New Issue
Block a user