Skip to content

Commit f3ed76a

Browse files
committed
feat(tui): add VS Code-style command palette with : key
- New CommandPaletteView: centered modal with fuzzy search, category grouping, and keyboard navigation (j/k, Enter, Esc) - 21 browser-mode commands across 6 categories (Navigation, View, Action, Check, Tab Management, Other) - Simplify hint bar from 15 items to 6 core hints + : commands entry - Add LLM anchor line [mxcli:commands] with all available operations in Faint styling for AI agent consumption - PaletteExecMsg dispatches selected command key back through Update (supports special keys: Space, Tab, multi-char sequences like ]e)
1 parent 6055c11 commit f3ed76a

6 files changed

Lines changed: 597 additions & 16 deletions

File tree

cmd/mxcli/tui/app.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func (a *App) syncBrowserView() {
132132
// Ensure miller has current dimensions so scroll calculations in
133133
// Update() work correctly (Render operates on a value copy).
134134
if a.height > 0 {
135-
contentH := max(5, a.height-chromeHeight)
135+
contentH := max(5, a.height-chromeHeight-1) // -1 for LLM anchor line
136136
bv.miller.SetSize(a.width, contentH)
137137
}
138138
a.views.SetBase(bv)
@@ -170,6 +170,13 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
170170
a.views.Pop()
171171
return a, nil
172172

173+
case PaletteExecMsg:
174+
a.views.Pop()
175+
if msg.Key != "" {
176+
return a, a.dispatchPaletteKey(msg.Key)
177+
}
178+
return a, nil
179+
173180
// --- View creation messages ---
174181
case OpenOverlayMsg:
175182
ov := NewOverlayView(msg.Title, msg.Content, a.width, a.height, msg.Opts)
@@ -452,7 +459,7 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
452459
// scroll calculations (Render operates on a copy and cannot
453460
// persist dimensions back).
454461
if bv, ok := a.views.Active().(BrowserView); ok {
455-
contentH := a.height - chromeHeight
462+
contentH := a.height - chromeHeight - 1 // -1 for LLM anchor line
456463
if contentH < 5 {
457464
contentH = 5
458465
}
@@ -474,7 +481,7 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
474481
bv.allNodes = msg.Nodes
475482
bv.miller = tab.Miller
476483
if a.height > 0 {
477-
contentH := max(5, a.height-chromeHeight)
484+
contentH := max(5, a.height-chromeHeight-1) // -1 for LLM anchor line
478485
bv.miller.SetSize(a.width, contentH)
479486
}
480487
a.views.SetBase(bv)
@@ -727,6 +734,11 @@ func (a *App) handleBrowserAppKeys(msg tea.KeyMsg) tea.Cmd {
727734
a.views.Push(ov)
728735
return handledCmd
729736

737+
case ":":
738+
cp := NewCommandPaletteView(a.width, a.height)
739+
a.views.Push(cp)
740+
return handledCmd
741+
730742
case "c":
731743
cv := NewCompareView()
732744
cv.mxcliPath = a.mxcliPath
@@ -820,8 +832,8 @@ func (a App) View() string {
820832
}
821833
}
822834

823-
// Content area
824-
contentH := a.height - chromeHeight
835+
// Content area (chromeHeight + 1 for the LLM anchor line)
836+
contentH := a.height - chromeHeight - 1
825837
if contentH < 5 {
826838
contentH = 5
827839
}
@@ -849,7 +861,11 @@ func (a App) View() string {
849861
a.statusBar.SetViewDepth(a.views.Depth(), viewModeNames)
850862
statusLine := StatusBarStyle.Width(a.width).Render(a.statusBar.View(a.width))
851863

852-
rendered := tabLine + "\n" + content + "\n" + hintLine + "\n" + statusLine
864+
// LLM anchor: machine-readable command list (Faint, not visible to users in practice)
865+
anchorStyle := lipgloss.NewStyle().Foreground(MutedColor).Faint(true)
866+
anchorLine := anchorStyle.Render("[mxcli:commands] h:back l:open Space:jump /:filter b:bson c:compare d:diagram z:zen Tab:toggle x:exec r:refresh y:copy !:check ]e:next-error [e:prev-error t:tab T:new-tab W:close-tab 1-9:switch ?:help ::palette")
867+
868+
rendered := anchorLine + "\n" + tabLine + "\n" + content + "\n" + hintLine + "\n" + statusLine
853869

854870
if a.showHelp {
855871
helpView := renderHelp(a.width, a.height)
@@ -939,6 +955,21 @@ func (a App) collectViewModeNames() []string {
939955
return a.views.ModeNames()
940956
}
941957

958+
// dispatchPaletteKey converts a palette command key string into a synthetic
959+
// tea.KeyMsg and re-dispatches it through Update.
960+
func (a App) dispatchPaletteKey(key string) tea.Cmd {
961+
var keyMsg tea.KeyMsg
962+
switch key {
963+
case " ":
964+
keyMsg = tea.KeyMsg{Type: tea.KeySpace}
965+
case "Tab":
966+
keyMsg = tea.KeyMsg{Type: tea.KeyTab}
967+
default:
968+
keyMsg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)}
969+
}
970+
return func() tea.Msg { return keyMsg }
971+
}
972+
942973
// inferBsonType maps tree node types to valid bson object types.
943974
func inferBsonType(nodeType string) string {
944975
switch strings.ToLower(nodeType) {

0 commit comments

Comments
 (0)