mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 11:37:20 +08:00
Merge pull request #222 from DedSecer/fix/cursor-command-prompt-replay
fix(prompt): replay selected Cursor commands
This commit is contained in:
@@ -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 ""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user