Merge pull request #222 from DedSecer/fix/cursor-command-prompt-replay

fix(prompt): replay selected Cursor commands
This commit is contained in:
leokun
2026-07-30 20:30:01 +08:00
committed by GitHub
6 changed files with 184 additions and 6 deletions
+40
View File
@@ -62,6 +62,46 @@ func buildUserReplayMessage(text string, selectedContext *agentv1.SelectedContex
}, true }, 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 ""
}
entries := make([]string, 0, len(selectedContext.GetCursorCommands()))
for _, command := range selectedContext.GetCursorCommands() {
if command == nil {
continue
}
content := strings.TrimSpace(command.GetContent())
if content == "" {
continue
}
name := strings.TrimSpace(command.GetName())
if name == "" {
entries = append(entries, "<cursor_command>\n"+content+"\n</cursor_command>")
continue
}
entries = append(entries, fmt.Sprintf("<cursor_command name=\"%s\">\n%s\n</cursor_command>", escapePromptXML(name), content))
}
if len(entries) == 0 {
return ""
}
return "<cursor_commands>\n" + strings.Join(entries, "\n\n") + "\n</cursor_commands>"
}
func buildSelectedIDEStatePromptSection(selectedContext *agentv1.SelectedContext) string { func buildSelectedIDEStatePromptSection(selectedContext *agentv1.SelectedContext) string {
if selectedContext == nil || selectedContext.GetInvocationContext() == nil { if selectedContext == nil || selectedContext.GetInvocationContext() == nil {
return "" return ""
+54 -5
View File
@@ -77,7 +77,7 @@ func (service *Service) maybeCompactBeforeProvider(stream *ActiveStream, convers
if service == nil || stream == nil || conversation == nil { if service == nil || stream == nil || conversation == nil {
return false, nil return false, nil
} }
manualInstruction, manual := parseManualCompactionDirective(stream.LatestUserText) manualInstruction, manual := streamManualCompactionDirective(stream)
plan, err := service.buildCompactionPlan(stream, conversation, compiled, manual, manualInstruction) plan, err := service.buildCompactionPlan(stream, conversation, compiled, manual, manualInstruction)
if err != nil { if err != nil {
return false, err return false, err
@@ -827,7 +827,7 @@ func buildFallbackCompactionSummary(plan *PendingCompaction) string {
sections = append(sections, "Compaction note:\n"+truncateCompactionText(plan.HookMessage, 800)) sections = append(sections, "Compaction note:\n"+truncateCompactionText(plan.HookMessage, 800))
} }
if strings.TrimSpace(plan.ManualInstruction) != "" { if strings.TrimSpace(plan.ManualInstruction) != "" {
sections = append(sections, "Manual compact instruction:\n"+truncateCompactionText(plan.ManualInstruction, 800)) sections = append(sections, "Manual summarize instruction:\n"+truncateCompactionText(plan.ManualInstruction, 800))
} }
return strings.TrimSpace(truncateCompactionText(strings.Join(sections, "\n\n"), compactionSummaryMaxChars)) return strings.TrimSpace(truncateCompactionText(strings.Join(sections, "\n\n"), compactionSummaryMaxChars))
} }
@@ -875,13 +875,62 @@ func (service *Service) resolveCompactionReserveTokens(modelID string) int64 {
return compactionAutoReserveTokens return compactionAutoReserveTokens
} }
func parseManualCompactionRequest(userMessage *agentv1.UserMessage) (string, bool) {
if userMessage == nil {
return "", false
}
userText := strings.TrimSpace(userMessage.GetText())
if instruction, ok := parseManualCompactionDirective(userText); ok {
return instruction, true
}
if userText != "" {
return "", false
}
selectedContext := userMessage.GetSelectedContext()
if selectedContext == nil {
return "", false
}
for _, command := range selectedContext.GetCursorCommands() {
if !isCursorSummarizeCommand(command) {
continue
}
instruction, _ := parseManualCompactionDirective(command.GetContent())
return instruction, true
}
return "", false
}
func streamManualCompactionDirective(stream *ActiveStream) (string, bool) {
if stream == nil {
return "", false
}
stream.mu.Lock()
defer stream.mu.Unlock()
if stream.ManualCompaction.Requested {
return strings.TrimSpace(stream.ManualCompaction.Instruction), true
}
return parseManualCompactionDirective(stream.LatestUserText)
}
func isCursorSummarizeCommand(command *agentv1.SelectedCursorCommand) bool {
if command == nil {
return false
}
if strings.EqualFold(strings.TrimSpace(command.GetName()), "glass-action-summarize") {
return true
}
_, ok := parseManualCompactionDirective(command.GetContent())
return ok
}
func parseManualCompactionDirective(latestUserText string) (string, bool) { func parseManualCompactionDirective(latestUserText string) (string, bool) {
trimmed := strings.TrimSpace(latestUserText) trimmed := strings.TrimSpace(latestUserText)
const directive = "/summarize"
switch { switch {
case trimmed == "/compact": case trimmed == directive:
return "", true return "", true
case strings.HasPrefix(trimmed, "/compact "): case strings.HasPrefix(trimmed, directive+" "):
return strings.TrimSpace(strings.TrimPrefix(trimmed, "/compact")), true return strings.TrimSpace(strings.TrimPrefix(trimmed, directive)), true
default: default:
return "", false return "", false
} }
@@ -9,6 +9,8 @@ import (
modeladapter "cursor/internal/backend/agent/model" modeladapter "cursor/internal/backend/agent/model"
) )
const promptContextSourceSelectedCursorCommands = "selected_cursor_commands"
func newPromptContextMessage(source string, message modeladapter.Message, persist bool) PromptContextMessage { func newPromptContextMessage(source string, message modeladapter.Message, persist bool) PromptContextMessage {
context := PromptContextMessage{ context := PromptContextMessage{
Source: strings.TrimSpace(source), Source: strings.TrimSpace(source),
@@ -18,6 +18,10 @@ const (
promptGuardSelectedFileChars = 16000 promptGuardSelectedFileChars = 16000
promptGuardSelectedFilesTotalChars = 64000 promptGuardSelectedFilesTotalChars = 64000
promptGuardSelectedFilesMaxCount = 12 promptGuardSelectedFilesMaxCount = 12
promptGuardCursorCommandNameChars = 256
promptGuardCursorCommandChars = 12000
promptGuardCursorCommandsTotalChars = 32000
promptGuardCursorCommandsMaxCount = 8
promptGuardRequestFileChars = 16000 promptGuardRequestFileChars = 16000
promptGuardRequestFilesTotalChars = 64000 promptGuardRequestFilesTotalChars = 64000
promptGuardRequestFilesMaxCount = 12 promptGuardRequestFilesMaxCount = 12
@@ -106,11 +110,42 @@ func guardSelectedContext(selectedContext *agentv1.SelectedContext) *agentv1.Sel
return selectedContext return selectedContext
} }
cloned.Files = guardSelectedFiles(cloned.GetFiles()) cloned.Files = guardSelectedFiles(cloned.GetFiles())
cloned.CursorCommands = guardSelectedCursorCommands(cloned.GetCursorCommands())
cloned.SelectedSkills = guardAgentSkills(cloned.GetSelectedSkills()) cloned.SelectedSkills = guardAgentSkills(cloned.GetSelectedSkills())
cloned.ExtraContext = guardStringSlice(cloned.GetExtraContext(), "selected_context.extra_context", promptGuardRealtimeTextChars, promptGuardRealtimeTextChars, promptGuardAgentSkillsMaxCount) cloned.ExtraContext = guardStringSlice(cloned.GetExtraContext(), "selected_context.extra_context", promptGuardRealtimeTextChars, promptGuardRealtimeTextChars, promptGuardAgentSkillsMaxCount)
return cloned 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 { func guardSelectedFiles(files []*agentv1.SelectedFile) []*agentv1.SelectedFile {
if len(files) == 0 { if len(files) == 0 {
return nil return nil
+46 -1
View File
@@ -23,6 +23,7 @@ import (
interactionbridge "cursor/internal/backend/agent/bridge/interaction" interactionbridge "cursor/internal/backend/agent/bridge/interaction"
runtimecore "cursor/internal/backend/agent/core" runtimecore "cursor/internal/backend/agent/core"
modeladapter "cursor/internal/backend/agent/model" modeladapter "cursor/internal/backend/agent/model"
promptengine "cursor/internal/backend/agent/prompt"
protocol "cursor/internal/backend/agent/protocol" protocol "cursor/internal/backend/agent/protocol"
) )
@@ -672,6 +673,7 @@ func (service *Service) decodeInboundIntent(requestID string, message *agentv1.A
default: default:
return InboundIntent{}, fmt.Errorf("unsupported client message kind: %s", clientKind) return InboundIntent{}, fmt.Errorf("unsupported client message kind: %s", clientKind)
} }
intent.ManualCompaction = resolveInboundManualCompaction(message, intent.UserMessage)
return intent, nil return intent, nil
} }
@@ -751,6 +753,7 @@ func (service *Service) handleRunIntent(intent InboundIntent) error {
stream.mu.Lock() stream.mu.Lock()
stream.ThinkingEffort = strings.TrimSpace(intent.ThinkingEffort) stream.ThinkingEffort = strings.TrimSpace(intent.ThinkingEffort)
stream.SubagentModelOverrides = cloneSubagentModelOverrides(intent.SubagentModelOverrides) stream.SubagentModelOverrides = cloneSubagentModelOverrides(intent.SubagentModelOverrides)
stream.ManualCompaction = intent.ManualCompaction
stream.PendingProviderAction = providerActionNone stream.PendingProviderAction = providerActionNone
stream.PendingCompaction = nil stream.PendingCompaction = nil
stream.PendingExecs = make(map[string]runtimecore.PendingExec) stream.PendingExecs = make(map[string]runtimecore.PendingExec)
@@ -788,6 +791,7 @@ func (service *Service) handleRunIntent(intent InboundIntent) error {
"subagent_model_override_count": len(intent.SubagentModelOverrides), "subagent_model_override_count": len(intent.SubagentModelOverrides),
"subagent_model_overrides": subagentModelOverrideSummaries(intent.SubagentModelOverrides), "subagent_model_overrides": subagentModelOverrideSummaries(intent.SubagentModelOverrides),
"latest_user_text": userMessageText(intent.UserMessage), "latest_user_text": userMessageText(intent.UserMessage),
"manual_compaction_requested": intent.ManualCompaction.Requested,
}) })
if err := service.publishCheckpoint(intent.RequestID, intent.ConversationID); err != nil { if err := service.publishCheckpoint(intent.RequestID, intent.ConversationID); err != nil {
return err return err
@@ -2332,7 +2336,8 @@ func buildRunEntries(intent InboundIntent, effectiveMode agentv1.AgentMode, turn
} }
} }
if intent.UserMessage != nil { if intent.UserMessage != nil {
payload, err := protojson.Marshal(normalizeUserMessageForStorage(intent.UserMessage)) normalized := normalizeUserMessageForStorage(intent.UserMessage)
payload, err := protojson.Marshal(normalized)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -2343,6 +2348,13 @@ func buildRunEntries(intent InboundIntent, effectiveMode agentv1.AgentMode, turn
Kind: "user_message", Kind: "user_message",
Payload: payload, 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) modeEntry, err := newModeMetadataEntry(turnSeq, intent.RequestID, effectiveMode, intent.HasExplicitMode, intent.ModeSource)
if err != nil { if err != nil {
@@ -2643,6 +2655,38 @@ func conversationActionIsResume(action *agentv1.ConversationAction) bool {
return ok return ok
} }
func inboundConversationAction(message *agentv1.AgentClientMessage) *agentv1.ConversationAction {
if message == nil {
return nil
}
if action := message.GetConversationAction(); action != nil {
return action
}
if runRequest := message.GetRunRequest(); runRequest != nil {
return runRequest.GetAction()
}
return nil
}
func conversationActionIsSummarize(action *agentv1.ConversationAction) bool {
if action == nil {
return false
}
_, ok := action.GetAction().(*agentv1.ConversationAction_SummarizeAction)
return ok
}
func resolveInboundManualCompaction(message *agentv1.AgentClientMessage, userMessage *agentv1.UserMessage) manualCompactionDirective {
instruction, requested := parseManualCompactionRequest(userMessage)
if conversationActionIsSummarize(inboundConversationAction(message)) {
requested = true
}
return manualCompactionDirective{
Requested: requested,
Instruction: instruction,
}
}
func conversationActionStartsRun(action *agentv1.ConversationAction) bool { func conversationActionStartsRun(action *agentv1.ConversationAction) bool {
if action == nil { if action == nil {
return false return false
@@ -2650,6 +2694,7 @@ func conversationActionStartsRun(action *agentv1.ConversationAction) bool {
switch action.GetAction().(type) { switch action.GetAction().(type) {
case *agentv1.ConversationAction_UserMessageAction, case *agentv1.ConversationAction_UserMessageAction,
*agentv1.ConversationAction_ResumeAction, *agentv1.ConversationAction_ResumeAction,
*agentv1.ConversationAction_SummarizeAction,
*agentv1.ConversationAction_StartPlanAction, *agentv1.ConversationAction_StartPlanAction,
*agentv1.ConversationAction_ExecutePlanAction: *agentv1.ConversationAction_ExecutePlanAction:
return true return true
+7
View File
@@ -116,6 +116,11 @@ type StreamSubscriber struct {
Signal chan struct{} Signal chan struct{}
} }
type manualCompactionDirective struct {
Requested bool
Instruction string
}
type ActiveStream struct { type ActiveStream struct {
mu sync.Mutex mu sync.Mutex
@@ -126,6 +131,7 @@ type ActiveStream struct {
ModelName string ModelName string
Mode agentv1.AgentMode Mode agentv1.AgentMode
LatestUserText string LatestUserText string
ManualCompaction manualCompactionDirective
Status StreamStatus Status StreamStatus
ThinkingEffort string ThinkingEffort string
SubagentModelOverrides map[string]runtimecore.SubagentModelOverrideSelection SubagentModelOverrides map[string]runtimecore.SubagentModelOverrideSelection
@@ -407,6 +413,7 @@ type InboundIntent struct {
HasExplicitMode bool HasExplicitMode bool
ModeSource ModeSource ModeSource ModeSource
StartsRun bool StartsRun bool
ManualCompaction manualCompactionDirective
SubagentTypeName string SubagentTypeName string
SubagentModelOverrides map[string]runtimecore.SubagentModelOverrideSelection SubagentModelOverrides map[string]runtimecore.SubagentModelOverrideSelection
ConversationState *agentv1.ConversationStateStructure ConversationState *agentv1.ConversationStateStructure