mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(tui): offer local command completion before kernel replies
This commit is contained in:
1 parent
44e9492be4
commit
041f40672d
2 files changed
+98
-5
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
@@ -85,14 +86,17 @@ func (m *model) refreshMenu() tea.Cmd {
|
||||
m.menu = nil
|
||||
return nil
|
||||
}
|
||||
m.onCompletion(completionMsg{line: line})
|
||||
return m.fetchCompletion()
|
||||
}
|
||||
|
||||
func (m *model) onCompletion(msg completionMsg) {
|
||||
if msg.err != nil || msg.line != m.composer.Value() {
|
||||
m.menu = nil
|
||||
if msg.line != m.composer.Value() {
|
||||
return
|
||||
}
|
||||
if msg.err != nil {
|
||||
msg.c = Completion{}
|
||||
}
|
||||
c := msg.c
|
||||
c.Items = append(m.localCommands(msg.line), c.Items...)
|
||||
if c.Typed != nil {
|
||||
@@ -105,7 +109,11 @@ func (m *model) onCompletion(msg completionMsg) {
|
||||
if len(msg.c.Items) == 0 {
|
||||
c.From, c.To = 0, utf16At(msg.line, len(msg.line))
|
||||
}
|
||||
m.menu = &menu{line: msg.line, c: c}
|
||||
sel := 0
|
||||
if m.menu != nil && m.menu.line == msg.line {
|
||||
sel = max(0, slices.Index(c.Items, m.menu.c.Items[m.menu.sel]))
|
||||
}
|
||||
m.menu = &menu{line: msg.line, c: c, sel: sel}
|
||||
}
|
||||
|
||||
// localCommands are the slash commands this screen answers itself, offered
|
||||
@@ -145,7 +153,8 @@ func (m *model) localCommands(line string) []CompletionItem {
|
||||
|
||||
// menuKey takes the keys an open menu owns.
|
||||
func (m *model) menuKey(k string) (tea.Cmd, bool) {
|
||||
if m.menu == nil {
|
||||
if m.menu == nil || m.menu.line != m.composer.Value() {
|
||||
m.menu = nil
|
||||
return nil, false
|
||||
}
|
||||
n := len(m.menu.c.Items)
|
||||
@@ -201,7 +210,7 @@ func (m *model) acceptCompletion() tea.Cmd {
|
||||
}
|
||||
|
||||
func (m *model) menuLines() []string {
|
||||
if m.menu == nil {
|
||||
if m.menu == nil || m.menu.line != m.composer.Value() {
|
||||
return nil
|
||||
}
|
||||
items := m.menu.c.Items
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
func TestTabCompletesLocalCommandsBeforeAnyKernelReply(t *testing.T) {
|
||||
for _, tc := range []struct{ typed, want string }{
|
||||
{"/resu", "/resume"},
|
||||
{"/ver", "/version"},
|
||||
{"/cop", "/copy"},
|
||||
} {
|
||||
t.Run(tc.typed, func(t *testing.T) {
|
||||
m, k := testModel(t)
|
||||
typeText(m, tc.typed)
|
||||
m.Update(tea.KeyPressMsg{Code: tea.KeyTab})
|
||||
if got := m.composer.Value(); got != tc.want {
|
||||
t.Fatalf("composer after immediate Tab = %q, want %q", got, tc.want)
|
||||
}
|
||||
if calledWith(k, "GET /complete") {
|
||||
t.Fatal("local completion waited for a kernel request")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaleKernelCompletionKeepsTheCurrentLocalMenu(t *testing.T) {
|
||||
m, _ := testModel(t)
|
||||
_, old := m.Update(tea.KeyPressMsg{Code: '/', Text: "/"})
|
||||
typeText(m, "resu")
|
||||
run(m, old)
|
||||
m.Update(tea.KeyPressMsg{Code: tea.KeyTab})
|
||||
if got := m.composer.Value(); got != "/resume" {
|
||||
t.Fatalf("composer after stale reply and Tab = %q, want /resume", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKernelReplyKeepsTheHighlightedLocalCommand(t *testing.T) {
|
||||
m, _ := testModel(t)
|
||||
_, pending := m.Update(tea.KeyPressMsg{Code: '/', Text: "/"})
|
||||
press(m, "down")
|
||||
run(m, pending)
|
||||
m.Update(tea.KeyPressMsg{Code: tea.KeyTab})
|
||||
if got := m.composer.Value(); got != "/queue" {
|
||||
t.Fatalf("composer after Down, reply, Tab = %q, want /queue", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalCompletionSurvivesAKernelFailure(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
m := newModel(context.Background(), Options{Client: &Client{HTTP: srv.Client(), Base: srv.URL}})
|
||||
m.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
|
||||
typeText(m, "/res")
|
||||
_, cmd := m.Update(tea.KeyPressMsg{Code: 'u', Text: "u"})
|
||||
run(m, cmd)
|
||||
m.Update(tea.KeyPressMsg{Code: tea.KeyTab})
|
||||
if got := m.composer.Value(); got != "/resume" {
|
||||
t.Fatalf("composer after failed lookup and Tab = %q, want /resume", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTabAfterClearingDoesNotAcceptThePreviousMenu(t *testing.T) {
|
||||
m, _ := testModel(t)
|
||||
typeText(m, "/resu")
|
||||
press(m, "ctrl+c")
|
||||
if rows := m.menuLines(); len(rows) != 0 {
|
||||
t.Fatalf("the cleared input still shows its old menu: %v", rows)
|
||||
}
|
||||
_, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyTab})
|
||||
if got := m.composer.Value(); got != "" {
|
||||
t.Fatalf("Tab restored the cleared command: %q", got)
|
||||
}
|
||||
if cmd == nil {
|
||||
t.Fatal("Tab did not request completions for the cleared input")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user