fix(acp): drop cursor placeholder slash-command descriptions (#2781)
This commit is contained in:
@@ -116,3 +116,29 @@ func MigrateCursorModel(
|
||||
}
|
||||
return bareModelID, merged, true
|
||||
}
|
||||
|
||||
// NormalizeCommandDescription drops cursor-agent's placeholder slash-command
|
||||
// description. cursor-agent emits a bare dash run ("---") as the description
|
||||
// for project/user slash commands that have no real description; passing it
|
||||
// through renders a meaningless "---" in the command palette, so treat it as
|
||||
// absent. The rewrite is Cursor-scoped: another agent may legitimately
|
||||
// advertise a dashes-only description, so descriptions from non-Cursor agents
|
||||
// are returned unchanged.
|
||||
//
|
||||
// This lives here, rather than next to either caller, because both the live
|
||||
// session adapter and the host-utility capability probe surface the same
|
||||
// commands and must agree; a cleanup applied to only one path leaves the other
|
||||
// rendering the placeholder.
|
||||
func NormalizeCommandDescription(agentID, description string) string {
|
||||
if agentID != CursorAgentID {
|
||||
return description
|
||||
}
|
||||
trimmed := strings.TrimSpace(description)
|
||||
if trimmed == "" {
|
||||
return description
|
||||
}
|
||||
if strings.Trim(trimmed, "-") == "" {
|
||||
return ""
|
||||
}
|
||||
return description
|
||||
}
|
||||
|
||||
@@ -107,3 +107,36 @@ func TestMigrateCursorModel_PreservesExplicitOptions(t *testing.T) {
|
||||
t.Error("MigrateCursorModel mutated the input options")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeCommandDescription_CursorClearsDashPlaceholder(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{name: "single dash", in: "---", want: ""},
|
||||
{name: "padded dash run", in: " --- ", want: ""},
|
||||
{name: "one dash", in: "-", want: ""},
|
||||
{name: "real description", in: "Commit changes", want: "Commit changes"},
|
||||
{name: "already empty", in: "", want: ""},
|
||||
{name: "dash in text kept", in: "foo - bar", want: "foo - bar"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := NormalizeCommandDescription(CursorAgentID, tc.in); got != tc.want {
|
||||
t.Errorf("NormalizeCommandDescription(cursor, %q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A dashes-only description from a non-Cursor agent is legitimate and must be
|
||||
// returned unchanged; the placeholder cleanup is Cursor-scoped.
|
||||
func TestNormalizeCommandDescription_OtherAgentsUnchanged(t *testing.T) {
|
||||
for _, agentID := range []string{"claude-acp", "codex-acp", GrokAgentID, ""} {
|
||||
if got := NormalizeCommandDescription(agentID, "---"); got != "---" {
|
||||
t.Errorf("NormalizeCommandDescription(%q, %q) = %q, want %q (non-Cursor untouched)",
|
||||
agentID, "---", got, "---")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/coder/acp-go-sdk"
|
||||
"github.com/kandev/kandev/internal/agentctl/acpcompat"
|
||||
"github.com/kandev/kandev/internal/agentctl/server/adapter/transport/shared"
|
||||
"github.com/kandev/kandev/internal/agentctl/types/streams"
|
||||
"go.uber.org/zap"
|
||||
@@ -654,7 +655,7 @@ func (a *Adapter) convertAvailableCommands(sessionID string, update *acp.Session
|
||||
seen[cmd.Name] = struct{}{}
|
||||
ac := streams.AvailableCommand{
|
||||
Name: cmd.Name,
|
||||
Description: cmd.Description,
|
||||
Description: acpcompat.NormalizeCommandDescription(a.agentID, cmd.Description),
|
||||
}
|
||||
if cmd.Input != nil && cmd.Input.Unstructured != nil {
|
||||
ac.InputHint = cmd.Input.Unstructured.Hint
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/coder/acp-go-sdk"
|
||||
"github.com/kandev/kandev/internal/agentctl/acpcompat"
|
||||
"github.com/kandev/kandev/internal/agentctl/server/adapter/transport/shared"
|
||||
"github.com/kandev/kandev/internal/agentctl/types/streams"
|
||||
"github.com/kandev/kandev/internal/common/logger"
|
||||
@@ -1079,6 +1080,62 @@ func TestConvertAvailableCommands_NoDuplicates(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertAvailableCommands_CursorPlaceholderDescriptionCleared(t *testing.T) {
|
||||
a := newTestAdapterForAgent(acpcompat.CursorAgentID)
|
||||
// cursor-agent sends a bare dash run for commands with no real description.
|
||||
update := &acp.SessionAvailableCommandsUpdate{
|
||||
AvailableCommands: []acp.AvailableCommand{
|
||||
{Name: "bootstrap-go-service", Description: "---"},
|
||||
{Name: "preferences", Description: " --- "},
|
||||
{Name: "commit", Description: "Commit changes"},
|
||||
{Name: "worktree", Description: ""},
|
||||
},
|
||||
}
|
||||
|
||||
result := a.convertAvailableCommands("session-placeholder", update)
|
||||
|
||||
if len(result.AvailableCommands) != 4 {
|
||||
t.Fatalf("expected 4 commands, got %d", len(result.AvailableCommands))
|
||||
}
|
||||
if result.AvailableCommands[0].Description != "" {
|
||||
t.Errorf("command[0].Description = %q, want empty (dash placeholder cleared)",
|
||||
result.AvailableCommands[0].Description)
|
||||
}
|
||||
if result.AvailableCommands[1].Description != "" {
|
||||
t.Errorf("command[1].Description = %q, want empty (padded dash placeholder cleared)",
|
||||
result.AvailableCommands[1].Description)
|
||||
}
|
||||
if result.AvailableCommands[2].Description != "Commit changes" {
|
||||
t.Errorf("command[2].Description = %q, want %q (real description preserved)",
|
||||
result.AvailableCommands[2].Description, "Commit changes")
|
||||
}
|
||||
if result.AvailableCommands[3].Description != "" {
|
||||
t.Errorf("command[3].Description = %q, want empty (already empty preserved)",
|
||||
result.AvailableCommands[3].Description)
|
||||
}
|
||||
}
|
||||
|
||||
// A non-Cursor agent that legitimately advertises a dashes-only description
|
||||
// keeps it: the placeholder cleanup is Cursor-scoped.
|
||||
func TestConvertAvailableCommands_NonCursorDescriptionPreserved(t *testing.T) {
|
||||
a := newTestAdapterForAgent("claude-acp")
|
||||
update := &acp.SessionAvailableCommandsUpdate{
|
||||
AvailableCommands: []acp.AvailableCommand{
|
||||
{Name: "rule", Description: "---"},
|
||||
},
|
||||
}
|
||||
|
||||
result := a.convertAvailableCommands("session-noncursor", update)
|
||||
|
||||
if len(result.AvailableCommands) != 1 {
|
||||
t.Fatalf("expected 1 command, got %d", len(result.AvailableCommands))
|
||||
}
|
||||
if result.AvailableCommands[0].Description != "---" {
|
||||
t.Errorf("command[0].Description = %q, want %q (non-Cursor description untouched)",
|
||||
result.AvailableCommands[0].Description, "---")
|
||||
}
|
||||
}
|
||||
|
||||
// --- generated usage/session-info updates ---
|
||||
|
||||
func TestConvertNotification_UsageUpdateFixture(t *testing.T) {
|
||||
|
||||
@@ -653,6 +653,7 @@ func isOpenCodeModelID(id string) bool {
|
||||
}
|
||||
|
||||
type acpProbeNotificationState struct {
|
||||
agentID string
|
||||
mu sync.Mutex
|
||||
commands []ProbeCommand
|
||||
configOptions []acp.SessionConfigOption
|
||||
@@ -661,8 +662,9 @@ type acpProbeNotificationState struct {
|
||||
gotConfigOptions chan struct{}
|
||||
}
|
||||
|
||||
func newACPProbeNotificationState() *acpProbeNotificationState {
|
||||
func newACPProbeNotificationState(agentID string) *acpProbeNotificationState {
|
||||
return &acpProbeNotificationState{
|
||||
agentID: agentID,
|
||||
gotCommands: make(chan struct{}, 1),
|
||||
gotConfigOptions: make(chan struct{}, 1),
|
||||
}
|
||||
@@ -685,7 +687,7 @@ func (s *acpProbeNotificationState) handle(n acp.SessionNotification) {
|
||||
for _, command := range update.AvailableCommands {
|
||||
s.commands = append(s.commands, ProbeCommand{
|
||||
Name: command.Name,
|
||||
Description: command.Description,
|
||||
Description: acpcompat.NormalizeCommandDescription(s.agentID, command.Description),
|
||||
})
|
||||
}
|
||||
s.mu.Unlock()
|
||||
@@ -843,7 +845,7 @@ func (e *ACPInferenceExecutor) probeACPSessionWithContext(
|
||||
mode string,
|
||||
requestedConfigOptions map[string]string,
|
||||
) (*ProbeResponse, error) {
|
||||
updates := newACPProbeNotificationState()
|
||||
updates := newACPProbeNotificationState(agentID)
|
||||
|
||||
client := acpclient.NewClient(
|
||||
acpclient.WithLogger(e.logger),
|
||||
|
||||
Reference in New Issue
Block a user