diff --git a/internal/backend/agent/prompt/replay.go b/internal/backend/agent/prompt/replay.go
index 68560b7..4b2eb0c 100644
--- a/internal/backend/agent/prompt/replay.go
+++ b/internal/backend/agent/prompt/replay.go
@@ -26,13 +26,10 @@ func BuildUserMessageReplayMessage(userMessage *agentv1.UserMessage) (Message, b
func buildUserReplayMessage(text string, selectedContext *agentv1.SelectedContext) (Message, bool) {
images := buildSelectedImageContentParts(selectedContext)
- sections := make([]string, 0, 5)
+ sections := make([]string, 0, 4)
if text != "" {
sections = append(sections, formatMessageText(fmt.Sprintf("\n%s\n", text)))
}
- if cursorCommands := buildSelectedCursorCommandsPromptSection(selectedContext); cursorCommands != "" {
- sections = append(sections, cursorCommands)
- }
if ideState := buildSelectedIDEStatePromptSection(selectedContext); ideState != "" {
sections = append(sections, ideState)
}
@@ -65,6 +62,20 @@ func buildUserReplayMessage(text string, selectedContext *agentv1.SelectedContex
}, true
}
+// BuildSelectedCursorCommandsReplayMessage renders command content for new history entries.
+// Keeping this separate from BuildUserMessageReplayMessage prevents old user_message entries
+// from changing their model-visible meaning after a backend upgrade.
+func BuildSelectedCursorCommandsReplayMessage(userMessage *agentv1.UserMessage) (Message, bool) {
+ if userMessage == nil {
+ return Message{}, false
+ }
+ content := buildSelectedCursorCommandsPromptSection(userMessage.GetSelectedContext())
+ if content == "" {
+ return Message{}, false
+ }
+ return Message{Role: "user", Content: content}, true
+}
+
func buildSelectedCursorCommandsPromptSection(selectedContext *agentv1.SelectedContext) string {
if selectedContext == nil || len(selectedContext.GetCursorCommands()) == 0 {
return ""
diff --git a/internal/backend/agent/prompt/replay_test.go b/internal/backend/agent/prompt/replay_test.go
index fc91b90..04abd85 100644
--- a/internal/backend/agent/prompt/replay_test.go
+++ b/internal/backend/agent/prompt/replay_test.go
@@ -2,67 +2,9 @@ package promptengine
import (
"reflect"
- "strings"
"testing"
-
- "cursor/gen/agentv1"
)
-func TestBuildUserMessageReplayMessageIncludesSelectedCursorCommands(t *testing.T) {
- message, ok := BuildUserMessageReplayMessage(&agentv1.UserMessage{
- Text: "/init",
- SelectedContext: &agentv1.SelectedContext{
- CursorCommands: []*agentv1.SelectedCursorCommand{
- {Name: "init", Content: "Analyze the repository and create AGENTS.md."},
- {Name: `review"<&`, Content: "Review the implementation."},
- },
- },
- })
- if !ok {
- t.Fatal("BuildUserMessageReplayMessage() returned ok=false")
- }
-
- want := strings.Join([]string{
- "\n/init\n",
- "\n" +
- "\nAnalyze the repository and create AGENTS.md.\n\n\n" +
- "\nReview the implementation.\n\n" +
- "",
- }, "\n\n")
- if message.Role != "user" || message.Content != want {
- t.Fatalf("message = %#v, want content %q", message, want)
- }
-}
-
-func TestBuildUserMessageReplayMessageSkipsEmptyCursorCommandsAndKeepsOrder(t *testing.T) {
- message, ok := BuildUserMessageReplayMessage(&agentv1.UserMessage{
- Text: "run commands",
- SelectedContext: &agentv1.SelectedContext{
- CursorCommands: []*agentv1.SelectedCursorCommand{
- nil,
- {Name: "empty", Content: " "},
- {Content: "First command."},
- {Name: "second", Content: "Second command."},
- },
- },
- })
- if !ok {
- t.Fatal("BuildUserMessageReplayMessage() returned ok=false")
- }
-
- first := strings.Index(message.Content, "First command.")
- second := strings.Index(message.Content, "Second command.")
- if first < 0 || second < 0 || first >= second {
- t.Fatalf("cursor command order was not preserved: %q", message.Content)
- }
- if strings.Contains(message.Content, "empty") {
- t.Fatalf("empty cursor command was not skipped: %q", message.Content)
- }
- if !strings.Contains(message.Content, "\nFirst command.\n") {
- t.Fatalf("unnamed cursor command was not rendered safely: %q", message.Content)
- }
-}
-
func TestBuildReplayMessagesFromPendingAssistantOutputsKeepsTextAndToolCallInOneAssistantTurn(t *testing.T) {
raw := `{
"id":"1",
diff --git a/internal/backend/forwarder/compaction_test.go b/internal/backend/forwarder/compaction_test.go
deleted file mode 100644
index c65131c..0000000
--- a/internal/backend/forwarder/compaction_test.go
+++ /dev/null
@@ -1,198 +0,0 @@
-package forwarder
-
-import (
- "testing"
-
- "cursor/gen/agentv1"
-)
-
-func TestParseManualCompactionDirectiveSupportsCursorSummarize(t *testing.T) {
- tests := []struct {
- name string
- text string
- wantInstruction string
- want bool
- }{
- {name: "compact removed", text: "/compact", want: false},
- {name: "compact instruction removed", text: "/compact keep deployment details", want: false},
- {name: "summarize", text: "/summarize", want: true},
- {name: "summarize instruction", text: "/summarize keep failing tests", wantInstruction: "keep failing tests", want: true},
- {name: "surrounding whitespace", text: " /summarize ", want: true},
- {name: "similar command", text: "/summarized", want: false},
- {name: "ordinary text", text: "please summarize this file", want: false},
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- instruction, ok := parseManualCompactionDirective(test.text)
- if ok != test.want || instruction != test.wantInstruction {
- t.Fatalf("parseManualCompactionDirective(%q) = (%q, %v), want (%q, %v)", test.text, instruction, ok, test.wantInstruction, test.want)
- }
- })
- }
-}
-
-func TestParseManualCompactionRequestRecognizesCursorSummarizeCommand(t *testing.T) {
- instruction, ok := parseManualCompactionRequest(&agentv1.UserMessage{
- SelectedContext: &agentv1.SelectedContext{
- CursorCommands: []*agentv1.SelectedCursorCommand{
- {Name: "glass-action-summarize", Content: "/summarize"},
- },
- },
- })
- if !ok || instruction != "" {
- t.Fatalf("parseManualCompactionRequest() = (%q, %v), want empty instruction and true", instruction, ok)
- }
-}
-
-func TestParseManualCompactionRequestIgnoresSummarizeMetadataWhenUserTextIsPresent(t *testing.T) {
- instruction, ok := parseManualCompactionRequest(&agentv1.UserMessage{
- Text: "why does /summarize not work?",
- SelectedContext: &agentv1.SelectedContext{
- CursorCommands: []*agentv1.SelectedCursorCommand{
- {Name: "glass-action-summarize", Content: "/summarize"},
- },
- },
- })
- if ok || instruction != "" {
- t.Fatalf("parseManualCompactionRequest() = (%q, %v), want empty instruction and false", instruction, ok)
- }
-}
-
-func TestParseManualCompactionRequestIgnoresOrdinaryCursorCommands(t *testing.T) {
- instruction, ok := parseManualCompactionRequest(&agentv1.UserMessage{
- Text: "review this implementation",
- SelectedContext: &agentv1.SelectedContext{
- CursorCommands: []*agentv1.SelectedCursorCommand{
- nil,
- {Name: "review", Content: "Review the implementation."},
- },
- },
- })
- if ok || instruction != "" {
- t.Fatalf("parseManualCompactionRequest() = (%q, %v), want empty instruction and false", instruction, ok)
- }
-}
-
-func TestDecodeInboundIntentMapsRunRequestSummarizeActionToManualCompaction(t *testing.T) {
- service := &Service{debug: newDebugRecorder("", nil, nil)}
- intent, err := service.decodeInboundIntent(
- "summarize-request",
- newRunRequestMessage(newSummarizeConversationAction()),
- "run_request",
- )
- if err != nil {
- t.Fatalf("decodeInboundIntent() error = %v", err)
- }
- if intent.Kind != "run" || !intent.StartsRun {
- t.Fatalf("decodeInboundIntent() kind = %q, starts_run = %v, want run and true", intent.Kind, intent.StartsRun)
- }
- if intent.UserMessage != nil {
- t.Fatalf("decodeInboundIntent() user_message = %#v, want nil", intent.UserMessage)
- }
- if !intent.ManualCompaction.Requested || intent.ManualCompaction.Instruction != "" {
- t.Fatalf("decodeInboundIntent() manual_compaction = %#v, want requested with empty instruction", intent.ManualCompaction)
- }
-}
-
-func TestResolveInboundManualCompactionSupportsStandaloneConversationAction(t *testing.T) {
- directive := resolveInboundManualCompaction(&agentv1.AgentClientMessage{
- Message: &agentv1.AgentClientMessage_ConversationAction{
- ConversationAction: newSummarizeConversationAction(),
- },
- }, nil)
- if !directive.Requested || directive.Instruction != "" {
- t.Fatalf("resolveInboundManualCompaction() = %#v, want requested with empty instruction", directive)
- }
-}
-
-func TestResolveInboundManualCompactionIgnoresOtherStandaloneActions(t *testing.T) {
- directive := resolveInboundManualCompaction(&agentv1.AgentClientMessage{
- Message: &agentv1.AgentClientMessage_ConversationAction{
- ConversationAction: &agentv1.ConversationAction{
- Action: &agentv1.ConversationAction_CancelAction{CancelAction: &agentv1.CancelAction{}},
- },
- },
- }, nil)
- if directive.Requested {
- t.Fatalf("resolveInboundManualCompaction() = %#v, want not requested", directive)
- }
-}
-
-func TestDecodeInboundIntentDoesNotMapOrdinaryRunActionsToManualCompaction(t *testing.T) {
- tests := []struct {
- name string
- text string
- }{
- {name: "ordinary message", text: "review this implementation"},
- {name: "compact command removed", text: "/compact"},
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- service := &Service{debug: newDebugRecorder("", nil, nil)}
- intent, err := service.decodeInboundIntent(
- "ordinary-request",
- newRunRequestMessage(&agentv1.ConversationAction{
- Action: &agentv1.ConversationAction_UserMessageAction{
- UserMessageAction: &agentv1.UserMessageAction{
- UserMessage: &agentv1.UserMessage{Text: test.text},
- },
- },
- }),
- "run_request",
- )
- if err != nil {
- t.Fatalf("decodeInboundIntent() error = %v", err)
- }
- if intent.ManualCompaction.Requested {
- t.Fatalf("decodeInboundIntent() manual_compaction = %#v, want not requested", intent.ManualCompaction)
- }
- })
- }
-}
-
-func TestSummarizeConversationActionStartsRun(t *testing.T) {
- if !conversationActionStartsRun(newSummarizeConversationAction()) {
- t.Fatal("conversationActionStartsRun(summarize) = false, want true")
- }
- if conversationActionStartsRun(&agentv1.ConversationAction{
- Action: &agentv1.ConversationAction_CancelAction{CancelAction: &agentv1.CancelAction{}},
- }) {
- t.Fatal("conversationActionStartsRun(cancel) = true, want false")
- }
-}
-
-func TestStreamManualCompactionDirectiveUsesStructuredRequest(t *testing.T) {
- stream := &ActiveStream{
- LatestUserText: "visible user text",
- ManualCompaction: manualCompactionDirective{
- Requested: true,
- Instruction: "keep decisions",
- },
- }
- instruction, ok := streamManualCompactionDirective(stream)
- if !ok || instruction != "keep decisions" {
- t.Fatalf("streamManualCompactionDirective() = (%q, %v), want (%q, true)", instruction, ok, "keep decisions")
- }
-}
-
-func newRunRequestMessage(action *agentv1.ConversationAction) *agentv1.AgentClientMessage {
- conversationID := "test-conversation"
- return &agentv1.AgentClientMessage{
- Message: &agentv1.AgentClientMessage_RunRequest{
- RunRequest: &agentv1.AgentRunRequest{
- ConversationId: &conversationID,
- Action: action,
- },
- },
- }
-}
-
-func newSummarizeConversationAction() *agentv1.ConversationAction {
- return &agentv1.ConversationAction{
- Action: &agentv1.ConversationAction_SummarizeAction{
- SummarizeAction: &agentv1.SummarizeAction{},
- },
- }
-}
diff --git a/internal/backend/forwarder/prompt_context.go b/internal/backend/forwarder/prompt_context.go
index 798dcda..f282a04 100644
--- a/internal/backend/forwarder/prompt_context.go
+++ b/internal/backend/forwarder/prompt_context.go
@@ -9,6 +9,8 @@ import (
modeladapter "cursor/internal/backend/agent/model"
)
+const promptContextSourceSelectedCursorCommands = "selected_cursor_commands"
+
func newPromptContextMessage(source string, message modeladapter.Message, persist bool) PromptContextMessage {
context := PromptContextMessage{
Source: strings.TrimSpace(source),
diff --git a/internal/backend/forwarder/prompt_guard.go b/internal/backend/forwarder/prompt_guard.go
index 3545325..a3bcc99 100644
--- a/internal/backend/forwarder/prompt_guard.go
+++ b/internal/backend/forwarder/prompt_guard.go
@@ -18,6 +18,10 @@ const (
promptGuardSelectedFileChars = 16000
promptGuardSelectedFilesTotalChars = 64000
promptGuardSelectedFilesMaxCount = 12
+ promptGuardCursorCommandNameChars = 256
+ promptGuardCursorCommandChars = 12000
+ promptGuardCursorCommandsTotalChars = 32000
+ promptGuardCursorCommandsMaxCount = 8
promptGuardRequestFileChars = 16000
promptGuardRequestFilesTotalChars = 64000
promptGuardRequestFilesMaxCount = 12
@@ -106,11 +110,42 @@ func guardSelectedContext(selectedContext *agentv1.SelectedContext) *agentv1.Sel
return selectedContext
}
cloned.Files = guardSelectedFiles(cloned.GetFiles())
+ cloned.CursorCommands = guardSelectedCursorCommands(cloned.GetCursorCommands())
cloned.SelectedSkills = guardAgentSkills(cloned.GetSelectedSkills())
cloned.ExtraContext = guardStringSlice(cloned.GetExtraContext(), "selected_context.extra_context", promptGuardRealtimeTextChars, promptGuardRealtimeTextChars, promptGuardAgentSkillsMaxCount)
return cloned
}
+func guardSelectedCursorCommands(commands []*agentv1.SelectedCursorCommand) []*agentv1.SelectedCursorCommand {
+ if len(commands) == 0 {
+ return nil
+ }
+ result := make([]*agentv1.SelectedCursorCommand, 0, minInt(len(commands), promptGuardCursorCommandsMaxCount))
+ remaining := promptGuardCursorCommandsTotalChars
+ for _, command := range commands {
+ if command == nil || len(result) >= promptGuardCursorCommandsMaxCount {
+ continue
+ }
+ content := strings.TrimSpace(command.GetContent())
+ if content == "" {
+ continue
+ }
+ limit := minInt(promptGuardCursorCommandChars, remaining)
+ if limit <= 0 {
+ break
+ }
+ cloned, ok := proto.Clone(command).(*agentv1.SelectedCursorCommand)
+ if !ok || cloned == nil {
+ continue
+ }
+ cloned.Name = truncatePromptGuardText("selected_context.cursor_commands.name", strings.TrimSpace(cloned.GetName()), promptGuardCursorCommandNameChars)
+ cloned.Content = truncatePromptGuardText("selected_context.cursor_commands.content", content, limit)
+ remaining -= promptGuardRuneCount(cloned.GetContent())
+ result = append(result, cloned)
+ }
+ return result
+}
+
func guardSelectedFiles(files []*agentv1.SelectedFile) []*agentv1.SelectedFile {
if len(files) == 0 {
return nil
diff --git a/internal/backend/forwarder/service.go b/internal/backend/forwarder/service.go
index 5909c8e..09c2a1a 100644
--- a/internal/backend/forwarder/service.go
+++ b/internal/backend/forwarder/service.go
@@ -23,6 +23,7 @@ import (
interactionbridge "cursor/internal/backend/agent/bridge/interaction"
runtimecore "cursor/internal/backend/agent/core"
modeladapter "cursor/internal/backend/agent/model"
+ promptengine "cursor/internal/backend/agent/prompt"
protocol "cursor/internal/backend/agent/protocol"
)
@@ -2335,7 +2336,8 @@ func buildRunEntries(intent InboundIntent, effectiveMode agentv1.AgentMode, turn
}
}
if intent.UserMessage != nil {
- payload, err := protojson.Marshal(normalizeUserMessageForStorage(intent.UserMessage))
+ normalized := normalizeUserMessageForStorage(intent.UserMessage)
+ payload, err := protojson.Marshal(normalized)
if err != nil {
return nil, err
}
@@ -2346,6 +2348,13 @@ func buildRunEntries(intent InboundIntent, effectiveMode agentv1.AgentMode, turn
Kind: "user_message",
Payload: payload,
})
+ if commandMessage, ok := promptengine.BuildSelectedCursorCommandsReplayMessage(normalized); ok {
+ entries = append(entries, newPromptContextEntry(turnSeq, intent.RequestID, newPromptContextMessage(
+ promptContextSourceSelectedCursorCommands,
+ modeladapter.Message{Role: commandMessage.Role, Content: commandMessage.Content},
+ true,
+ )))
+ }
}
modeEntry, err := newModeMetadataEntry(turnSeq, intent.RequestID, effectiveMode, intent.HasExplicitMode, intent.ModeSource)
if err != nil {