feat: remove prompt completion feature (#1076)

This commit is contained in:
tanzhenxin
2025-11-20 14:36:51 +08:00
committed by GitHub
parent e1e7a0d606
commit 07069f00d1
11 changed files with 99 additions and 665 deletions

View File

@@ -789,7 +789,6 @@ export async function loadCliConfig(
useBuiltinRipgrep: settings.tools?.useBuiltinRipgrep, useBuiltinRipgrep: settings.tools?.useBuiltinRipgrep,
shouldUseNodePtyShell: settings.tools?.shell?.enableInteractiveShell, shouldUseNodePtyShell: settings.tools?.shell?.enableInteractiveShell,
skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck, skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
enablePromptCompletion: settings.general?.enablePromptCompletion ?? false,
skipLoopDetection: settings.model?.skipLoopDetection ?? false, skipLoopDetection: settings.model?.skipLoopDetection ?? false,
skipStartupContext: settings.model?.skipStartupContext ?? false, skipStartupContext: settings.model?.skipStartupContext ?? false,
vlmSwitchMode, vlmSwitchMode,

View File

@@ -77,7 +77,6 @@ const MIGRATION_MAP: Record<string, string> = {
disableAutoUpdate: 'general.disableAutoUpdate', disableAutoUpdate: 'general.disableAutoUpdate',
disableUpdateNag: 'general.disableUpdateNag', disableUpdateNag: 'general.disableUpdateNag',
dnsResolutionOrder: 'advanced.dnsResolutionOrder', dnsResolutionOrder: 'advanced.dnsResolutionOrder',
enablePromptCompletion: 'general.enablePromptCompletion',
enforcedAuthType: 'security.auth.enforcedType', enforcedAuthType: 'security.auth.enforcedType',
excludeTools: 'tools.exclude', excludeTools: 'tools.exclude',
excludeMCPServers: 'mcp.excluded', excludeMCPServers: 'mcp.excluded',

View File

@@ -167,16 +167,6 @@ const SETTINGS_SCHEMA = {
}, },
}, },
}, },
enablePromptCompletion: {
type: 'boolean',
label: 'Enable Prompt Completion',
category: 'General',
requiresRestart: true,
default: false,
description:
'Enable AI-powered prompt completion suggestions while typing.',
showInDialog: true,
},
debugKeystrokeLogging: { debugKeystrokeLogging: {
type: 'boolean', type: 'boolean',
label: 'Debug Keystroke Logging', label: 'Debug Keystroke Logging',

View File

@@ -164,11 +164,6 @@ describe('InputPrompt', () => {
setActiveSuggestionIndex: vi.fn(), setActiveSuggestionIndex: vi.fn(),
setShowSuggestions: vi.fn(), setShowSuggestions: vi.fn(),
handleAutocomplete: vi.fn(), handleAutocomplete: vi.fn(),
promptCompletion: {
text: '',
accept: vi.fn(),
clear: vi.fn(),
},
}; };
mockedUseCommandCompletion.mockReturnValue(mockCommandCompletion); mockedUseCommandCompletion.mockReturnValue(mockCommandCompletion);

View File

@@ -12,9 +12,8 @@ import { theme } from '../semantic-colors.js';
import { useInputHistory } from '../hooks/useInputHistory.js'; import { useInputHistory } from '../hooks/useInputHistory.js';
import type { TextBuffer } from './shared/text-buffer.js'; import type { TextBuffer } from './shared/text-buffer.js';
import { logicalPosToOffset } from './shared/text-buffer.js'; import { logicalPosToOffset } from './shared/text-buffer.js';
import { cpSlice, cpLen, toCodePoints } from '../utils/textUtils.js'; import { cpSlice, cpLen } from '../utils/textUtils.js';
import chalk from 'chalk'; import chalk from 'chalk';
import stringWidth from 'string-width';
import { useShellHistory } from '../hooks/useShellHistory.js'; import { useShellHistory } from '../hooks/useShellHistory.js';
import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js'; import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js';
import { useCommandCompletion } from '../hooks/useCommandCompletion.js'; import { useCommandCompletion } from '../hooks/useCommandCompletion.js';
@@ -91,7 +90,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
commandContext, commandContext,
placeholder = ' Type your message or @path/to/file', placeholder = ' Type your message or @path/to/file',
focus = true, focus = true,
inputWidth,
suggestionsWidth, suggestionsWidth,
shellModeActive, shellModeActive,
setShellModeActive, setShellModeActive,
@@ -526,16 +524,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
} }
} }
// Handle Tab key for ghost text acceptance
if (
key.name === 'tab' &&
!completion.showSuggestions &&
completion.promptCompletion.text
) {
completion.promptCompletion.accept();
return;
}
if (!shellModeActive) { if (!shellModeActive) {
if (keyMatchers[Command.REVERSE_SEARCH](key)) { if (keyMatchers[Command.REVERSE_SEARCH](key)) {
setCommandSearchActive(true); setCommandSearchActive(true);
@@ -657,18 +645,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
// Fall back to the text buffer's default input handling for all other keys // Fall back to the text buffer's default input handling for all other keys
buffer.handleInput(key); buffer.handleInput(key);
// Clear ghost text when user types regular characters (not navigation/control keys)
if (
completion.promptCompletion.text &&
key.sequence &&
key.sequence.length === 1 &&
!key.ctrl &&
!key.meta
) {
completion.promptCompletion.clear();
setExpandedSuggestionIndex(-1);
}
}, },
[ [
focus, focus,
@@ -703,118 +679,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
buffer.visualCursor; buffer.visualCursor;
const scrollVisualRow = buffer.visualScrollRow; const scrollVisualRow = buffer.visualScrollRow;
const getGhostTextLines = useCallback(() => {
if (
!completion.promptCompletion.text ||
!buffer.text ||
!completion.promptCompletion.text.startsWith(buffer.text)
) {
return { inlineGhost: '', additionalLines: [] };
}
const ghostSuffix = completion.promptCompletion.text.slice(
buffer.text.length,
);
if (!ghostSuffix) {
return { inlineGhost: '', additionalLines: [] };
}
const currentLogicalLine = buffer.lines[buffer.cursor[0]] || '';
const cursorCol = buffer.cursor[1];
const textBeforeCursor = cpSlice(currentLogicalLine, 0, cursorCol);
const usedWidth = stringWidth(textBeforeCursor);
const remainingWidth = Math.max(0, inputWidth - usedWidth);
const ghostTextLinesRaw = ghostSuffix.split('\n');
const firstLineRaw = ghostTextLinesRaw.shift() || '';
let inlineGhost = '';
let remainingFirstLine = '';
if (stringWidth(firstLineRaw) <= remainingWidth) {
inlineGhost = firstLineRaw;
} else {
const words = firstLineRaw.split(' ');
let currentLine = '';
let wordIdx = 0;
for (const word of words) {
const prospectiveLine = currentLine ? `${currentLine} ${word}` : word;
if (stringWidth(prospectiveLine) > remainingWidth) {
break;
}
currentLine = prospectiveLine;
wordIdx++;
}
inlineGhost = currentLine;
if (words.length > wordIdx) {
remainingFirstLine = words.slice(wordIdx).join(' ');
}
}
const linesToWrap = [];
if (remainingFirstLine) {
linesToWrap.push(remainingFirstLine);
}
linesToWrap.push(...ghostTextLinesRaw);
const remainingGhostText = linesToWrap.join('\n');
const additionalLines: string[] = [];
if (remainingGhostText) {
const textLines = remainingGhostText.split('\n');
for (const textLine of textLines) {
const words = textLine.split(' ');
let currentLine = '';
for (const word of words) {
const prospectiveLine = currentLine ? `${currentLine} ${word}` : word;
const prospectiveWidth = stringWidth(prospectiveLine);
if (prospectiveWidth > inputWidth) {
if (currentLine) {
additionalLines.push(currentLine);
}
let wordToProcess = word;
while (stringWidth(wordToProcess) > inputWidth) {
let part = '';
const wordCP = toCodePoints(wordToProcess);
let partWidth = 0;
let splitIndex = 0;
for (let i = 0; i < wordCP.length; i++) {
const char = wordCP[i];
const charWidth = stringWidth(char);
if (partWidth + charWidth > inputWidth) {
break;
}
part += char;
partWidth += charWidth;
splitIndex = i + 1;
}
additionalLines.push(part);
wordToProcess = cpSlice(wordToProcess, splitIndex);
}
currentLine = wordToProcess;
} else {
currentLine = prospectiveLine;
}
}
if (currentLine) {
additionalLines.push(currentLine);
}
}
}
return { inlineGhost, additionalLines };
}, [
completion.promptCompletion.text,
buffer.text,
buffer.lines,
buffer.cursor,
inputWidth,
]);
const { inlineGhost, additionalLines } = getGhostTextLines();
const getActiveCompletion = () => { const getActiveCompletion = () => {
if (commandSearchActive) return commandSearchCompletion; if (commandSearchActive) return commandSearchCompletion;
if (reverseSearchActive) return reverseSearchCompletion; if (reverseSearchActive) return reverseSearchCompletion;
@@ -887,134 +751,96 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
<Text color={theme.text.secondary}>{placeholder}</Text> <Text color={theme.text.secondary}>{placeholder}</Text>
) )
) : ( ) : (
linesToRender linesToRender.map((lineText, visualIdxInRenderedSet) => {
.map((lineText, visualIdxInRenderedSet) => { const absoluteVisualIdx =
const absoluteVisualIdx = scrollVisualRow + visualIdxInRenderedSet;
scrollVisualRow + visualIdxInRenderedSet; const mapEntry = buffer.visualToLogicalMap[absoluteVisualIdx];
const mapEntry = buffer.visualToLogicalMap[absoluteVisualIdx]; const cursorVisualRow = cursorVisualRowAbsolute - scrollVisualRow;
const cursorVisualRow = const isOnCursorLine =
cursorVisualRowAbsolute - scrollVisualRow; focus && visualIdxInRenderedSet === cursorVisualRow;
const isOnCursorLine =
focus && visualIdxInRenderedSet === cursorVisualRow;
const renderedLine: React.ReactNode[] = []; const renderedLine: React.ReactNode[] = [];
const [logicalLineIdx, logicalStartCol] = mapEntry; const [logicalLineIdx, logicalStartCol] = mapEntry;
const logicalLine = buffer.lines[logicalLineIdx] || ''; const logicalLine = buffer.lines[logicalLineIdx] || '';
const tokens = parseInputForHighlighting( const tokens = parseInputForHighlighting(
logicalLine, logicalLine,
logicalLineIdx, logicalLineIdx,
); );
const visualStart = logicalStartCol; const visualStart = logicalStartCol;
const visualEnd = logicalStartCol + cpLen(lineText); const visualEnd = logicalStartCol + cpLen(lineText);
const segments = buildSegmentsForVisualSlice( const segments = buildSegmentsForVisualSlice(
tokens, tokens,
visualStart, visualStart,
visualEnd, visualEnd,
); );
let charCount = 0; let charCount = 0;
segments.forEach((seg, segIdx) => { segments.forEach((seg, segIdx) => {
const segLen = cpLen(seg.text); const segLen = cpLen(seg.text);
let display = seg.text; let display = seg.text;
if (isOnCursorLine) { if (isOnCursorLine) {
const relativeVisualColForHighlight = const relativeVisualColForHighlight = cursorVisualColAbsolute;
cursorVisualColAbsolute; const segStart = charCount;
const segStart = charCount; const segEnd = segStart + segLen;
const segEnd = segStart + segLen; if (
if ( relativeVisualColForHighlight >= segStart &&
relativeVisualColForHighlight >= segStart && relativeVisualColForHighlight < segEnd
relativeVisualColForHighlight < segEnd ) {
) { const charToHighlight = cpSlice(
const charToHighlight = cpSlice( seg.text,
relativeVisualColForHighlight - segStart,
relativeVisualColForHighlight - segStart + 1,
);
const highlighted = showCursor
? chalk.inverse(charToHighlight)
: charToHighlight;
display =
cpSlice(
seg.text, seg.text,
0,
relativeVisualColForHighlight - segStart, relativeVisualColForHighlight - segStart,
) +
highlighted +
cpSlice(
seg.text,
relativeVisualColForHighlight - segStart + 1, relativeVisualColForHighlight - segStart + 1,
); );
const highlighted = showCursor
? chalk.inverse(charToHighlight)
: charToHighlight;
display =
cpSlice(
seg.text,
0,
relativeVisualColForHighlight - segStart,
) +
highlighted +
cpSlice(
seg.text,
relativeVisualColForHighlight - segStart + 1,
);
}
charCount = segEnd;
}
const color =
seg.type === 'command' || seg.type === 'file'
? theme.text.accent
: theme.text.primary;
renderedLine.push(
<Text key={`token-${segIdx}`} color={color}>
{display}
</Text>,
);
});
const currentLineGhost = isOnCursorLine ? inlineGhost : '';
if (
isOnCursorLine &&
cursorVisualColAbsolute === cpLen(lineText)
) {
if (!currentLineGhost) {
renderedLine.push(
<Text key={`cursor-end-${cursorVisualColAbsolute}`}>
{showCursor ? chalk.inverse(' ') : ' '}
</Text>,
);
} }
charCount = segEnd;
} }
const showCursorBeforeGhost = const color =
focus && seg.type === 'command' || seg.type === 'file'
isOnCursorLine && ? theme.text.accent
cursorVisualColAbsolute === cpLen(lineText) && : theme.text.primary;
currentLineGhost;
return ( renderedLine.push(
<Box key={`line-${visualIdxInRenderedSet}`} height={1}> <Text key={`token-${segIdx}`} color={color}>
<Text> {display}
{renderedLine} </Text>,
{showCursorBeforeGhost &&
(showCursor ? chalk.inverse(' ') : ' ')}
{currentLineGhost && (
<Text color={theme.text.secondary}>
{currentLineGhost}
</Text>
)}
</Text>
</Box>
); );
}) });
.concat(
additionalLines.map((ghostLine, index) => { if (
const padding = Math.max( isOnCursorLine &&
0, cursorVisualColAbsolute === cpLen(lineText)
inputWidth - stringWidth(ghostLine), ) {
); renderedLine.push(
return ( <Text key={`cursor-end-${cursorVisualColAbsolute}`}>
<Text {showCursor ? chalk.inverse(' ') : ' '}
key={`ghost-line-${index}`} </Text>,
color={theme.text.secondary} );
> }
{ghostLine}
{' '.repeat(padding)} return (
</Text> <Box key={`line-${visualIdxInRenderedSet}`} height={1}>
); <Text>{renderedLine}</Text>
}), </Box>
) );
})
)} )}
</Box> </Box>
</Box> </Box>

View File

@@ -1271,7 +1271,6 @@ describe('SettingsDialog', () => {
vimMode: true, vimMode: true,
disableAutoUpdate: true, disableAutoUpdate: true,
debugKeystrokeLogging: true, debugKeystrokeLogging: true,
enablePromptCompletion: true,
}, },
ui: { ui: {
hideWindowTitle: true, hideWindowTitle: true,
@@ -1517,7 +1516,6 @@ describe('SettingsDialog', () => {
vimMode: false, vimMode: false,
disableAutoUpdate: false, disableAutoUpdate: false,
debugKeystrokeLogging: false, debugKeystrokeLogging: false,
enablePromptCompletion: false,
}, },
ui: { ui: {
hideWindowTitle: false, hideWindowTitle: false,

View File

@@ -10,8 +10,6 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly
│ │ │ │
│ Disable Auto Update false │ │ Disable Auto Update false │
│ │ │ │
│ Enable Prompt Completion false │
│ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Output Format Text │ │ Output Format Text │
@@ -22,6 +20,8 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -44,8 +44,6 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select
│ │ │ │
│ Disable Auto Update false │ │ Disable Auto Update false │
│ │ │ │
│ Enable Prompt Completion false │
│ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Output Format Text │ │ Output Format Text │
@@ -56,6 +54,8 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -78,8 +78,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett
│ │ │ │
│ Disable Auto Update false │ │ Disable Auto Update false │
│ │ │ │
│ Enable Prompt Completion false │
│ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Output Format Text │ │ Output Format Text │
@@ -90,6 +88,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -112,8 +112,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin
│ │ │ │
│ Disable Auto Update false* │ │ Disable Auto Update false* │
│ │ │ │
│ Enable Prompt Completion false* │
│ │
│ Debug Keystroke Logging false* │ │ Debug Keystroke Logging false* │
│ │ │ │
│ Output Format Text │ │ Output Format Text │
@@ -124,6 +122,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin
│ │ │ │
│ Hide Tips false* │ │ Hide Tips false* │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -146,8 +146,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
│ │ │ │
│ Disable Auto Update (Modified in System) false │ │ Disable Auto Update (Modified in System) false │
│ │ │ │
│ Enable Prompt Completion false │
│ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Output Format Text │ │ Output Format Text │
@@ -158,6 +156,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -180,8 +180,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
│ │ │ │
│ Disable Auto Update false │ │ Disable Auto Update false │
│ │ │ │
│ Enable Prompt Completion false │
│ │
│ Debug Keystroke Logging (Modified in Workspace) false │ │ Debug Keystroke Logging (Modified in Workspace) false │
│ │ │ │
│ Output Format Text │ │ Output Format Text │
@@ -192,6 +190,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -214,8 +214,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set
│ │ │ │
│ Disable Auto Update false │ │ Disable Auto Update false │
│ │ │ │
│ Enable Prompt Completion false │
│ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Output Format Text │ │ Output Format Text │
@@ -226,6 +224,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -248,8 +248,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and
│ │ │ │
│ Disable Auto Update true* │ │ Disable Auto Update true* │
│ │ │ │
│ Enable Prompt Completion false │
│ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Output Format Text │ │ Output Format Text │
@@ -260,6 +258,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -282,8 +282,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security
│ │ │ │
│ Disable Auto Update false │ │ Disable Auto Update false │
│ │ │ │
│ Enable Prompt Completion false │
│ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Output Format Text │ │ Output Format Text │
@@ -294,6 +292,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -316,8 +316,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se
│ │ │ │
│ Disable Auto Update true* │ │ Disable Auto Update true* │
│ │ │ │
│ Enable Prompt Completion true* │
│ │
│ Debug Keystroke Logging true* │ │ Debug Keystroke Logging true* │
│ │ │ │
│ Output Format Text │ │ Output Format Text │
@@ -328,6 +326,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se
│ │ │ │
│ Hide Tips true* │ │ Hide Tips true* │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │

View File

@@ -83,9 +83,7 @@ const setupMocks = ({
describe('useCommandCompletion', () => { describe('useCommandCompletion', () => {
const mockCommandContext = {} as CommandContext; const mockCommandContext = {} as CommandContext;
const mockConfig = { const mockConfig = {} as Config;
getEnablePromptCompletion: () => false,
} as Config;
const testDirs: string[] = []; const testDirs: string[] = [];
const testRootDir = '/'; const testRootDir = '/';
@@ -516,81 +514,4 @@ describe('useCommandCompletion', () => {
); );
}); });
}); });
describe('prompt completion filtering', () => {
it('should not trigger prompt completion for line comments', async () => {
const mockConfig = {
getEnablePromptCompletion: () => true,
} as Config;
const { result } = renderHook(() => {
const textBuffer = useTextBufferForTest('// This is a line comment');
const completion = useCommandCompletion(
textBuffer,
testDirs,
testRootDir,
[],
mockCommandContext,
false,
mockConfig,
);
return { ...completion, textBuffer };
});
// Should not trigger prompt completion for comments
expect(result.current.suggestions.length).toBe(0);
});
it('should not trigger prompt completion for block comments', async () => {
const mockConfig = {
getEnablePromptCompletion: () => true,
} as Config;
const { result } = renderHook(() => {
const textBuffer = useTextBufferForTest(
'/* This is a block comment */',
);
const completion = useCommandCompletion(
textBuffer,
testDirs,
testRootDir,
[],
mockCommandContext,
false,
mockConfig,
);
return { ...completion, textBuffer };
});
// Should not trigger prompt completion for comments
expect(result.current.suggestions.length).toBe(0);
});
it('should trigger prompt completion for regular text when enabled', async () => {
const mockConfig = {
getEnablePromptCompletion: () => true,
} as Config;
const { result } = renderHook(() => {
const textBuffer = useTextBufferForTest(
'This is regular text that should trigger completion',
);
const completion = useCommandCompletion(
textBuffer,
testDirs,
testRootDir,
[],
mockCommandContext,
false,
mockConfig,
);
return { ...completion, textBuffer };
});
// This test verifies that comments are filtered out while regular text is not
expect(result.current.textBuffer.text).toBe(
'This is regular text that should trigger completion',
);
});
});
}); });

View File

@@ -13,11 +13,6 @@ import { isSlashCommand } from '../utils/commandUtils.js';
import { toCodePoints } from '../utils/textUtils.js'; import { toCodePoints } from '../utils/textUtils.js';
import { useAtCompletion } from './useAtCompletion.js'; import { useAtCompletion } from './useAtCompletion.js';
import { useSlashCompletion } from './useSlashCompletion.js'; import { useSlashCompletion } from './useSlashCompletion.js';
import type { PromptCompletion } from './usePromptCompletion.js';
import {
usePromptCompletion,
PROMPT_COMPLETION_MIN_LENGTH,
} from './usePromptCompletion.js';
import type { Config } from '@qwen-code/qwen-code-core'; import type { Config } from '@qwen-code/qwen-code-core';
import { useCompletion } from './useCompletion.js'; import { useCompletion } from './useCompletion.js';
@@ -25,7 +20,6 @@ export enum CompletionMode {
IDLE = 'IDLE', IDLE = 'IDLE',
AT = 'AT', AT = 'AT',
SLASH = 'SLASH', SLASH = 'SLASH',
PROMPT = 'PROMPT',
} }
export interface UseCommandCompletionReturn { export interface UseCommandCompletionReturn {
@@ -41,7 +35,6 @@ export interface UseCommandCompletionReturn {
navigateUp: () => void; navigateUp: () => void;
navigateDown: () => void; navigateDown: () => void;
handleAutocomplete: (indexToUse: number) => void; handleAutocomplete: (indexToUse: number) => void;
promptCompletion: PromptCompletion;
} }
export function useCommandCompletion( export function useCommandCompletion(
@@ -126,32 +119,13 @@ export function useCommandCompletion(
} }
} }
// Check for prompt completion - only if enabled
const trimmedText = buffer.text.trim();
const isPromptCompletionEnabled =
config?.getEnablePromptCompletion() ?? false;
if (
isPromptCompletionEnabled &&
trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH &&
!isSlashCommand(trimmedText) &&
!trimmedText.includes('@')
) {
return {
completionMode: CompletionMode.PROMPT,
query: trimmedText,
completionStart: 0,
completionEnd: trimmedText.length,
};
}
return { return {
completionMode: CompletionMode.IDLE, completionMode: CompletionMode.IDLE,
query: null, query: null,
completionStart: -1, completionStart: -1,
completionEnd: -1, completionEnd: -1,
}; };
}, [cursorRow, cursorCol, buffer.lines, buffer.text, config]); }, [cursorRow, cursorCol, buffer.lines]);
useAtCompletion({ useAtCompletion({
enabled: completionMode === CompletionMode.AT, enabled: completionMode === CompletionMode.AT,
@@ -172,12 +146,6 @@ export function useCommandCompletion(
setIsPerfectMatch, setIsPerfectMatch,
}); });
const promptCompletion = usePromptCompletion({
buffer,
config,
enabled: completionMode === CompletionMode.PROMPT,
});
useEffect(() => { useEffect(() => {
setActiveSuggestionIndex(suggestions.length > 0 ? 0 : -1); setActiveSuggestionIndex(suggestions.length > 0 ? 0 : -1);
setVisibleStartIndex(0); setVisibleStartIndex(0);
@@ -264,6 +232,5 @@ export function useCommandCompletion(
navigateUp, navigateUp,
navigateDown, navigateDown,
handleAutocomplete, handleAutocomplete,
promptCompletion,
}; };
} }

View File

@@ -1,254 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import type { Config } from '@qwen-code/qwen-code-core';
import {
DEFAULT_GEMINI_FLASH_LITE_MODEL,
getResponseText,
} from '@qwen-code/qwen-code-core';
import type { Content, GenerateContentConfig } from '@google/genai';
import type { TextBuffer } from '../components/shared/text-buffer.js';
import { isSlashCommand } from '../utils/commandUtils.js';
export const PROMPT_COMPLETION_MIN_LENGTH = 5;
export const PROMPT_COMPLETION_DEBOUNCE_MS = 250;
export interface PromptCompletion {
text: string;
isLoading: boolean;
isActive: boolean;
accept: () => void;
clear: () => void;
markSelected: (selectedText: string) => void;
}
export interface UsePromptCompletionOptions {
buffer: TextBuffer;
config?: Config;
enabled: boolean;
}
export function usePromptCompletion({
buffer,
config,
enabled,
}: UsePromptCompletionOptions): PromptCompletion {
const [ghostText, setGhostText] = useState<string>('');
const [isLoadingGhostText, setIsLoadingGhostText] = useState<boolean>(false);
const abortControllerRef = useRef<AbortController | null>(null);
const [justSelectedSuggestion, setJustSelectedSuggestion] =
useState<boolean>(false);
const lastSelectedTextRef = useRef<string>('');
const lastRequestedTextRef = useRef<string>('');
const isPromptCompletionEnabled =
enabled && (config?.getEnablePromptCompletion() ?? false);
const clearGhostText = useCallback(() => {
setGhostText('');
setIsLoadingGhostText(false);
}, []);
const acceptGhostText = useCallback(() => {
if (ghostText && ghostText.length > buffer.text.length) {
buffer.setText(ghostText);
setGhostText('');
setJustSelectedSuggestion(true);
lastSelectedTextRef.current = ghostText;
}
}, [ghostText, buffer]);
const markSuggestionSelected = useCallback((selectedText: string) => {
setJustSelectedSuggestion(true);
lastSelectedTextRef.current = selectedText;
}, []);
const generatePromptSuggestions = useCallback(async () => {
const trimmedText = buffer.text.trim();
const geminiClient = config?.getGeminiClient();
if (trimmedText === lastRequestedTextRef.current) {
return;
}
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
if (
trimmedText.length < PROMPT_COMPLETION_MIN_LENGTH ||
!geminiClient ||
isSlashCommand(trimmedText) ||
trimmedText.includes('@') ||
!isPromptCompletionEnabled
) {
clearGhostText();
lastRequestedTextRef.current = '';
return;
}
lastRequestedTextRef.current = trimmedText;
setIsLoadingGhostText(true);
abortControllerRef.current = new AbortController();
const signal = abortControllerRef.current.signal;
try {
const contents: Content[] = [
{
role: 'user',
parts: [
{
text: `You are a professional prompt engineering assistant. Complete the user's partial prompt with expert precision and clarity. User's input: "${trimmedText}" Continue this prompt by adding specific, actionable details that align with the user's intent. Focus on: clear, precise language; structured requirements; professional terminology; measurable outcomes. Length Guidelines: Keep suggestions concise (ideally 10-20 characters); prioritize brevity while maintaining clarity; use essential keywords only; avoid redundant phrases. Start your response with the exact user text ("${trimmedText}") followed by your completion. Provide practical, implementation-focused suggestions rather than creative interpretations. Format: Plain text only. Single completion. Match the user's language. Emphasize conciseness over elaboration.`,
},
],
},
];
const generationConfig: GenerateContentConfig = {
temperature: 0.3,
maxOutputTokens: 16000,
thinkingConfig: {
thinkingBudget: 0,
},
};
const response = await geminiClient.generateContent(
contents,
generationConfig,
signal,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
);
if (signal.aborted) {
return;
}
if (response) {
const responseText = getResponseText(response);
if (responseText) {
const suggestionText = responseText.trim();
if (
suggestionText.length > 0 &&
suggestionText.startsWith(trimmedText)
) {
setGhostText(suggestionText);
} else {
clearGhostText();
}
}
}
} catch (error) {
if (
!(
signal.aborted ||
(error instanceof Error && error.name === 'AbortError')
)
) {
console.error('prompt completion error:', error);
// Clear the last requested text to allow retry only on real errors
lastRequestedTextRef.current = '';
}
clearGhostText();
} finally {
if (!signal.aborted) {
setIsLoadingGhostText(false);
}
}
}, [buffer.text, config, clearGhostText, isPromptCompletionEnabled]);
const isCursorAtEnd = useCallback(() => {
const [cursorRow, cursorCol] = buffer.cursor;
const totalLines = buffer.lines.length;
if (cursorRow !== totalLines - 1) {
return false;
}
const lastLine = buffer.lines[cursorRow] || '';
return cursorCol === lastLine.length;
}, [buffer.cursor, buffer.lines]);
const handlePromptCompletion = useCallback(() => {
if (!isCursorAtEnd()) {
clearGhostText();
return;
}
const trimmedText = buffer.text.trim();
if (justSelectedSuggestion && trimmedText === lastSelectedTextRef.current) {
return;
}
if (trimmedText !== lastSelectedTextRef.current) {
setJustSelectedSuggestion(false);
lastSelectedTextRef.current = '';
}
generatePromptSuggestions();
}, [
buffer.text,
generatePromptSuggestions,
justSelectedSuggestion,
isCursorAtEnd,
clearGhostText,
]);
// Debounce prompt completion
useEffect(() => {
const timeoutId = setTimeout(
handlePromptCompletion,
PROMPT_COMPLETION_DEBOUNCE_MS,
);
return () => clearTimeout(timeoutId);
}, [buffer.text, buffer.cursor, handlePromptCompletion]);
// Ghost text validation - clear if it doesn't match current text or cursor not at end
useEffect(() => {
const currentText = buffer.text.trim();
if (ghostText && !isCursorAtEnd()) {
clearGhostText();
return;
}
if (
ghostText &&
currentText.length > 0 &&
!ghostText.startsWith(currentText)
) {
clearGhostText();
}
}, [buffer.text, buffer.cursor, ghostText, clearGhostText, isCursorAtEnd]);
// Cleanup on unmount
useEffect(() => () => abortControllerRef.current?.abort(), []);
const isActive = useMemo(() => {
if (!isPromptCompletionEnabled) return false;
if (!isCursorAtEnd()) return false;
const trimmedText = buffer.text.trim();
return (
trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH &&
!isSlashCommand(trimmedText) &&
!trimmedText.includes('@')
);
}, [buffer.text, isPromptCompletionEnabled, isCursorAtEnd]);
return {
text: ghostText,
isLoading: isLoadingGhostText,
isActive,
accept: acceptGhostText,
clear: clearGhostText,
markSelected: markSuggestionSelected,
};
}

View File

@@ -280,7 +280,6 @@ export interface ConfigParameters {
skipNextSpeakerCheck?: boolean; skipNextSpeakerCheck?: boolean;
shellExecutionConfig?: ShellExecutionConfig; shellExecutionConfig?: ShellExecutionConfig;
extensionManagement?: boolean; extensionManagement?: boolean;
enablePromptCompletion?: boolean;
skipLoopDetection?: boolean; skipLoopDetection?: boolean;
vlmSwitchMode?: string; vlmSwitchMode?: string;
truncateToolOutputThreshold?: number; truncateToolOutputThreshold?: number;
@@ -377,7 +376,6 @@ export class Config {
private readonly skipNextSpeakerCheck: boolean; private readonly skipNextSpeakerCheck: boolean;
private shellExecutionConfig: ShellExecutionConfig; private shellExecutionConfig: ShellExecutionConfig;
private readonly extensionManagement: boolean = true; private readonly extensionManagement: boolean = true;
private readonly enablePromptCompletion: boolean = false;
private readonly skipLoopDetection: boolean; private readonly skipLoopDetection: boolean;
private readonly skipStartupContext: boolean; private readonly skipStartupContext: boolean;
private readonly vlmSwitchMode: string | undefined; private readonly vlmSwitchMode: string | undefined;
@@ -495,7 +493,6 @@ export class Config {
this.useSmartEdit = params.useSmartEdit ?? false; this.useSmartEdit = params.useSmartEdit ?? false;
this.extensionManagement = params.extensionManagement ?? true; this.extensionManagement = params.extensionManagement ?? true;
this.storage = new Storage(this.targetDir); this.storage = new Storage(this.targetDir);
this.enablePromptCompletion = params.enablePromptCompletion ?? false;
this.vlmSwitchMode = params.vlmSwitchMode; this.vlmSwitchMode = params.vlmSwitchMode;
this.fileExclusions = new FileExclusions(this); this.fileExclusions = new FileExclusions(this);
this.eventEmitter = params.eventEmitter; this.eventEmitter = params.eventEmitter;
@@ -1038,10 +1035,6 @@ export class Config {
return this.accessibility.screenReader ?? false; return this.accessibility.screenReader ?? false;
} }
getEnablePromptCompletion(): boolean {
return this.enablePromptCompletion;
}
getSkipLoopDetection(): boolean { getSkipLoopDetection(): boolean {
return this.skipLoopDetection; return this.skipLoopDetection;
} }