feat(tui-go): slash command menu in the chat input
Typing `/` as the first char of a chat line opens the command palette inline (opencode-style), instead of typing a literal slash; mid-line `/` stays literal. Factors openPalette() (shared with the `p` key) and advertises `/ cmds` in the insert-mode footer when the line is empty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// `/` as the first char of a chat line opens the command palette inline (opencode-style),
|
||||
// without typing a literal slash.
|
||||
func TestSlashOpensPaletteOnEmptyLine(t *testing.T) {
|
||||
m := NewModel(nil)
|
||||
m.editMode = ModeInsert
|
||||
m.inputMode = ModeRouter
|
||||
m.inputBuffer = ""
|
||||
|
||||
next, _ := m.handleInsertKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")})
|
||||
nm := next.(Model)
|
||||
|
||||
if nm.overlay != OverlayPalette {
|
||||
t.Fatalf("'/' on an empty line should open the palette; overlay=%d", nm.overlay)
|
||||
}
|
||||
if nm.inputBuffer != "" {
|
||||
t.Fatalf("'/' should not be typed literally; buffer=%q", nm.inputBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
// Mid-line `/` is a literal slash (e.g. a path), not a command trigger.
|
||||
func TestSlashLiteralMidLine(t *testing.T) {
|
||||
m := NewModel(nil)
|
||||
m.editMode = ModeInsert
|
||||
m.inputMode = ModeRouter
|
||||
m.inputBuffer = "path"
|
||||
m.inputCursor = len("path")
|
||||
|
||||
next, _ := m.handleInsertKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")})
|
||||
nm := next.(Model)
|
||||
|
||||
if nm.overlay != OverlayNone {
|
||||
t.Fatalf("mid-line '/' must stay literal, not open the palette; overlay=%d", nm.overlay)
|
||||
}
|
||||
if nm.inputBuffer != "path/" {
|
||||
t.Fatalf("mid-line '/' should be typed; buffer=%q", nm.inputBuffer)
|
||||
}
|
||||
}
|
||||
@@ -229,9 +229,7 @@ func (m Model) handleNormalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case "k":
|
||||
m.navUp()
|
||||
case "p":
|
||||
m.overlay = OverlayPalette
|
||||
m.paletteFilter = ""
|
||||
m.paletteIndex = 0
|
||||
m.openPalette()
|
||||
case "e":
|
||||
m.overlay = OverlayEventInspector
|
||||
m.overlayEventIdx = 0
|
||||
@@ -408,12 +406,28 @@ func (m Model) handleInsertKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.historyNext()
|
||||
}
|
||||
case tea.KeyRunes, tea.KeySpace:
|
||||
// opencode-style slash commands: `/` as the first char of a chat line opens the
|
||||
// command palette inline instead of typing a literal slash. Mid-line `/` is literal.
|
||||
if m.inputMode == ModeRouter && m.inputBuffer == "" && string(k.Runes) == "/" {
|
||||
m.editMode = ModeNormal
|
||||
m.clearInput()
|
||||
m.openPalette()
|
||||
return m, nil
|
||||
}
|
||||
m.appendRunes(string(k.Runes))
|
||||
m.syncFilter()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// openPalette opens the command palette with a cleared filter. Shared by the `p`
|
||||
// normal-mode key and the inline `/` trigger in the chat input.
|
||||
func (m *Model) openPalette() {
|
||||
m.overlay = OverlayPalette
|
||||
m.paletteFilter = ""
|
||||
m.paletteIndex = 0
|
||||
}
|
||||
|
||||
// beginIntent stashes the chosen workflow and switches to a text-entry prompt for the
|
||||
// freeform request. Submitting sends StartSession(id, intent); an empty line starts with no
|
||||
// intent (fixed-task workflows). Esc cancels.
|
||||
|
||||
@@ -221,6 +221,10 @@ func (m Model) renderFooter() string {
|
||||
switch {
|
||||
case m.editMode == ModeInsert:
|
||||
hints = []string{hint("enter", "send"), hint("esc", "normal"), hint("↑↓", "history")}
|
||||
// The `/` command menu only triggers on an empty chat line (mid-line `/` is literal).
|
||||
if m.inputMode == ModeRouter && m.inputBuffer == "" {
|
||||
hints = append(hints, hint("/", "cmds"))
|
||||
}
|
||||
case m.displayState() == StateApproval:
|
||||
// Approval actions live in the band itself; the footer offers navigation only.
|
||||
hints = []string{hint("l", "back"), hint("e", "events"), hint("q", "quit")}
|
||||
|
||||
Reference in New Issue
Block a user