mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
refactor(cursor): Move stream dedup logic to CursorProvider
Move Cursor-specific duplicate text handling from auto-mode-service.ts into CursorProvider.deduplicateTextBlocks() for cleaner separation. This handles: - Duplicate consecutive text blocks (same text twice in a row) - Final accumulated text block (contains ALL previous text) Also update REFACTORING-ANALYSIS.md with SpawnStrategy types for future CLI providers (wsl, npx, direct, cmd). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import type {
|
||||
ProviderMessage,
|
||||
InstallationStatus,
|
||||
ModelDefinition,
|
||||
ContentBlock,
|
||||
} from './types.js';
|
||||
import {
|
||||
type CursorStreamEvent,
|
||||
@@ -451,6 +452,64 @@ export class CursorProvider extends BaseProvider {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduplicate text blocks in Cursor assistant messages
|
||||
*
|
||||
* Cursor often sends:
|
||||
* 1. Duplicate consecutive text blocks (same text twice in a row)
|
||||
* 2. A final accumulated block containing ALL previous text
|
||||
*
|
||||
* This method filters out these duplicates to prevent UI stuttering.
|
||||
*/
|
||||
private deduplicateTextBlocks(
|
||||
content: ContentBlock[],
|
||||
lastTextBlock: string,
|
||||
accumulatedText: string
|
||||
): { content: ContentBlock[]; lastBlock: string; accumulated: string } {
|
||||
const filtered: ContentBlock[] = [];
|
||||
let newLastBlock = lastTextBlock;
|
||||
let newAccumulated = accumulatedText;
|
||||
|
||||
for (const block of content) {
|
||||
if (block.type !== 'text' || !block.text) {
|
||||
filtered.push(block);
|
||||
continue;
|
||||
}
|
||||
|
||||
const text = block.text;
|
||||
|
||||
// Skip empty text
|
||||
if (!text.trim()) continue;
|
||||
|
||||
// Skip duplicate consecutive text blocks
|
||||
if (text === newLastBlock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip final accumulated text block
|
||||
// Cursor sends one large block containing ALL previous text at the end
|
||||
if (newAccumulated.length > 100 && text.length > newAccumulated.length * 0.8) {
|
||||
const normalizedAccum = newAccumulated.replace(/\s+/g, ' ').trim();
|
||||
const normalizedNew = text.replace(/\s+/g, ' ').trim();
|
||||
if (normalizedNew.includes(normalizedAccum.slice(0, 100))) {
|
||||
// This is the final accumulated block, skip it
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// This is a valid new text block
|
||||
newLastBlock = text;
|
||||
newAccumulated += text;
|
||||
filtered.push(block);
|
||||
}
|
||||
|
||||
return {
|
||||
content: filtered,
|
||||
lastBlock: newLastBlock,
|
||||
accumulated: newAccumulated,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Cursor event to AutoMaker ProviderMessage format
|
||||
*/
|
||||
@@ -706,6 +765,10 @@ export class CursorProvider extends BaseProvider {
|
||||
|
||||
let sessionId: string | undefined;
|
||||
|
||||
// Dedup state for Cursor-specific text block handling
|
||||
let lastTextBlock = '';
|
||||
let accumulatedText = '';
|
||||
|
||||
try {
|
||||
// spawnJSONLProcess yields parsed JSON objects, handles errors
|
||||
for await (const rawEvent of spawnJSONLProcess(subprocessOptions)) {
|
||||
@@ -724,6 +787,28 @@ export class CursorProvider extends BaseProvider {
|
||||
if (!normalized.session_id && sessionId) {
|
||||
normalized.session_id = sessionId;
|
||||
}
|
||||
|
||||
// Apply Cursor-specific dedup for assistant text messages
|
||||
if (normalized.type === 'assistant' && normalized.message?.content) {
|
||||
const dedupedContent = this.deduplicateTextBlocks(
|
||||
normalized.message.content,
|
||||
lastTextBlock,
|
||||
accumulatedText
|
||||
);
|
||||
|
||||
if (dedupedContent.content.length === 0) {
|
||||
// All blocks were duplicates, skip this message
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update state
|
||||
lastTextBlock = dedupedContent.lastBlock;
|
||||
accumulatedText = dedupedContent.accumulated;
|
||||
|
||||
// Update the message with deduped content
|
||||
normalized.message.content = dedupedContent.content;
|
||||
}
|
||||
|
||||
yield normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1981,9 +1981,6 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
}, WRITE_DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
// Track last text block for deduplication (Cursor sends duplicates)
|
||||
let lastTextBlock = '';
|
||||
|
||||
streamLoop: for await (const msg of stream) {
|
||||
// Log raw stream event for debugging
|
||||
appendRawEvent(msg);
|
||||
@@ -1996,30 +1993,8 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
// Skip empty text
|
||||
if (!newText) continue;
|
||||
|
||||
// Cursor-specific: Skip duplicate consecutive text blocks
|
||||
// Cursor often sends the same text twice in a row
|
||||
if (newText === lastTextBlock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Cursor-specific: Skip final accumulated text block
|
||||
// At the end, Cursor sends one large block containing ALL previous text
|
||||
// Detect by checking if this block contains most of responseText
|
||||
if (
|
||||
responseText.length > 100 &&
|
||||
newText.length > responseText.length * 0.8 &&
|
||||
responseText.trim().length > 0
|
||||
) {
|
||||
// Check if this looks like accumulated text (contains our existing content)
|
||||
const normalizedResponse = responseText.replace(/\s+/g, ' ').trim();
|
||||
const normalizedNew = newText.replace(/\s+/g, ' ').trim();
|
||||
if (normalizedNew.includes(normalizedResponse.slice(0, 100))) {
|
||||
// This is the final accumulated block, skip it
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
lastTextBlock = newText;
|
||||
// Note: Cursor-specific dedup (duplicate blocks, accumulated text) is now
|
||||
// handled in CursorProvider.deduplicateTextBlocks() for cleaner separation
|
||||
|
||||
// Only add separator when we're at a natural paragraph break:
|
||||
// - Previous text ends with sentence terminator AND new text starts a new thought
|
||||
|
||||
Reference in New Issue
Block a user