fix(acp): drop cursor placeholder slash-command descriptions (#2781)

This commit is contained in:
Carlos Florêncio
2026-08-18 22:45:19 +01:00
committed by GitHub
parent 9c5888abc8
commit 14797c2901
5 changed files with 123 additions and 4 deletions
@@ -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),