Enha: shellmode within inputfield

This commit is contained in:
Grail Finder
2026-02-27 07:58:00 +03:00
parent 09b5e0d08f
commit a0ff384b81
4 changed files with 153 additions and 15 deletions

View File

@@ -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,7 +442,11 @@ func updateFlexLayout() {
}
flex.Clear()
flex.AddItem(textView, 0, 40, false)
if shellMode {
flex.AddItem(shellInput, 0, 10, false)
} else {
flex.AddItem(textArea, 0, 10, false)
}
if positionVisible {
flex.AddItem(statusLineWidget, 0, 2, false)
}
@@ -451,6 +454,8 @@ func updateFlexLayout() {
focused := app.GetFocus()
if focused == textView {
app.SetFocus(textView)
} else if shellMode {
app.SetFocus(shellInput)
} else {
app.SetFocus(textArea)
}
@@ -515,6 +520,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

View File

@@ -13,6 +13,8 @@ var (
injectRole = true
selectedIndex = int(-1)
shellMode = false
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{}

View File

@@ -405,6 +405,66 @@ func showFileCompletionPopup(filter string) {
app.SetFocus(widget)
}
func showShellFileCompletionPopup(filter string) {
baseDir := cfg.FilePickerDir
if baseDir == "" {
baseDir = "."
}
complMatches := scanFiles(baseDir, filter)
if len(complMatches) == 0 {
return
}
if len(complMatches) == 1 {
currentText := shellInput.GetText()
atIdx := strings.LastIndex(currentText, "@")
if atIdx >= 0 {
before := currentText[:atIdx]
shellInput.SetText(before + complMatches[0])
}
return
}
widget := tview.NewList().ShowSecondaryText(false).
SetSelectedBackgroundColor(tcell.ColorGray)
widget.SetTitle("file completion").SetBorder(true)
for _, m := range complMatches {
widget.AddItem(m, "", 0, nil)
}
widget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
currentText := shellInput.GetText()
atIdx := strings.LastIndex(currentText, "@")
if atIdx >= 0 {
before := currentText[:atIdx]
shellInput.SetText(before + mainText)
}
pages.RemovePage("shellFileCompletionPopup")
app.SetFocus(shellInput)
})
widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape {
pages.RemovePage("shellFileCompletionPopup")
app.SetFocus(shellInput)
return nil
}
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
pages.RemovePage("shellFileCompletionPopup")
app.SetFocus(shellInput)
return nil
}
return event
})
modal := func(p tview.Primitive, width, height int) tview.Primitive {
return tview.NewFlex().
AddItem(nil, 0, 1, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(nil, 0, 1, false).
AddItem(p, height, 1, true).
AddItem(nil, 0, 1, false), width, 1, true).
AddItem(nil, 0, 1, false)
}
pages.AddPage("shellFileCompletionPopup", modal(widget, 80, 20), true, true)
app.SetFocus(widget)
}
func updateWidgetColors(theme *tview.Theme) {
bgColor := theme.PrimitiveBackgroundColor
fgColor := theme.PrimaryTextColor

80
tui.go
View File

@@ -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,12 +125,75 @@ 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()
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 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")
@@ -948,14 +1012,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