From 07069f00d1360a40191507552dddd62fdd32d822 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Thu, 20 Nov 2025 14:36:51 +0800 Subject: [PATCH] feat: remove prompt completion feature (#1076) --- packages/cli/src/config/config.ts | 1 - packages/cli/src/config/settings.ts | 1 - packages/cli/src/config/settingsSchema.ts | 10 - .../src/ui/components/InputPrompt.test.tsx | 5 - .../cli/src/ui/components/InputPrompt.tsx | 328 ++++-------------- .../src/ui/components/SettingsDialog.test.tsx | 2 - .../SettingsDialog.test.tsx.snap | 40 +-- .../src/ui/hooks/useCommandCompletion.test.ts | 81 +---- .../cli/src/ui/hooks/useCommandCompletion.tsx | 35 +- .../cli/src/ui/hooks/usePromptCompletion.ts | 254 -------------- packages/core/src/config/config.ts | 7 - 11 files changed, 99 insertions(+), 665 deletions(-) delete mode 100644 packages/cli/src/ui/hooks/usePromptCompletion.ts diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 50a11991..7286ff12 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -789,7 +789,6 @@ export async function loadCliConfig( useBuiltinRipgrep: settings.tools?.useBuiltinRipgrep, shouldUseNodePtyShell: settings.tools?.shell?.enableInteractiveShell, skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck, - enablePromptCompletion: settings.general?.enablePromptCompletion ?? false, skipLoopDetection: settings.model?.skipLoopDetection ?? false, skipStartupContext: settings.model?.skipStartupContext ?? false, vlmSwitchMode, diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index aefcb103..8ff022c8 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -77,7 +77,6 @@ const MIGRATION_MAP: Record = { disableAutoUpdate: 'general.disableAutoUpdate', disableUpdateNag: 'general.disableUpdateNag', dnsResolutionOrder: 'advanced.dnsResolutionOrder', - enablePromptCompletion: 'general.enablePromptCompletion', enforcedAuthType: 'security.auth.enforcedType', excludeTools: 'tools.exclude', excludeMCPServers: 'mcp.excluded', diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 70037dfd..e0ece3ac 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -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: { type: 'boolean', label: 'Debug Keystroke Logging', diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 38d5f7b1..25274a12 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -164,11 +164,6 @@ describe('InputPrompt', () => { setActiveSuggestionIndex: vi.fn(), setShowSuggestions: vi.fn(), handleAutocomplete: vi.fn(), - promptCompletion: { - text: '', - accept: vi.fn(), - clear: vi.fn(), - }, }; mockedUseCommandCompletion.mockReturnValue(mockCommandCompletion); diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index f33700d8..2bd9b275 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -12,9 +12,8 @@ import { theme } from '../semantic-colors.js'; import { useInputHistory } from '../hooks/useInputHistory.js'; import type { TextBuffer } 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 stringWidth from 'string-width'; import { useShellHistory } from '../hooks/useShellHistory.js'; import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js'; import { useCommandCompletion } from '../hooks/useCommandCompletion.js'; @@ -91,7 +90,6 @@ export const InputPrompt: React.FC = ({ commandContext, placeholder = ' Type your message or @path/to/file', focus = true, - inputWidth, suggestionsWidth, shellModeActive, setShellModeActive, @@ -526,16 +524,6 @@ export const InputPrompt: React.FC = ({ } } - // Handle Tab key for ghost text acceptance - if ( - key.name === 'tab' && - !completion.showSuggestions && - completion.promptCompletion.text - ) { - completion.promptCompletion.accept(); - return; - } - if (!shellModeActive) { if (keyMatchers[Command.REVERSE_SEARCH](key)) { setCommandSearchActive(true); @@ -657,18 +645,6 @@ export const InputPrompt: React.FC = ({ // Fall back to the text buffer's default input handling for all other keys 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, @@ -703,118 +679,6 @@ export const InputPrompt: React.FC = ({ buffer.visualCursor; 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 = () => { if (commandSearchActive) return commandSearchCompletion; if (reverseSearchActive) return reverseSearchCompletion; @@ -887,134 +751,96 @@ export const InputPrompt: React.FC = ({ {placeholder} ) ) : ( - linesToRender - .map((lineText, visualIdxInRenderedSet) => { - const absoluteVisualIdx = - scrollVisualRow + visualIdxInRenderedSet; - const mapEntry = buffer.visualToLogicalMap[absoluteVisualIdx]; - const cursorVisualRow = - cursorVisualRowAbsolute - scrollVisualRow; - const isOnCursorLine = - focus && visualIdxInRenderedSet === cursorVisualRow; + linesToRender.map((lineText, visualIdxInRenderedSet) => { + const absoluteVisualIdx = + scrollVisualRow + visualIdxInRenderedSet; + const mapEntry = buffer.visualToLogicalMap[absoluteVisualIdx]; + const cursorVisualRow = cursorVisualRowAbsolute - scrollVisualRow; + const isOnCursorLine = + focus && visualIdxInRenderedSet === cursorVisualRow; - const renderedLine: React.ReactNode[] = []; + const renderedLine: React.ReactNode[] = []; - const [logicalLineIdx, logicalStartCol] = mapEntry; - const logicalLine = buffer.lines[logicalLineIdx] || ''; - const tokens = parseInputForHighlighting( - logicalLine, - logicalLineIdx, - ); + const [logicalLineIdx, logicalStartCol] = mapEntry; + const logicalLine = buffer.lines[logicalLineIdx] || ''; + const tokens = parseInputForHighlighting( + logicalLine, + logicalLineIdx, + ); - const visualStart = logicalStartCol; - const visualEnd = logicalStartCol + cpLen(lineText); - const segments = buildSegmentsForVisualSlice( - tokens, - visualStart, - visualEnd, - ); + const visualStart = logicalStartCol; + const visualEnd = logicalStartCol + cpLen(lineText); + const segments = buildSegmentsForVisualSlice( + tokens, + visualStart, + visualEnd, + ); - let charCount = 0; - segments.forEach((seg, segIdx) => { - const segLen = cpLen(seg.text); - let display = seg.text; + let charCount = 0; + segments.forEach((seg, segIdx) => { + const segLen = cpLen(seg.text); + let display = seg.text; - if (isOnCursorLine) { - const relativeVisualColForHighlight = - cursorVisualColAbsolute; - const segStart = charCount; - const segEnd = segStart + segLen; - if ( - relativeVisualColForHighlight >= segStart && - relativeVisualColForHighlight < segEnd - ) { - const charToHighlight = cpSlice( + if (isOnCursorLine) { + const relativeVisualColForHighlight = cursorVisualColAbsolute; + const segStart = charCount; + const segEnd = segStart + segLen; + if ( + relativeVisualColForHighlight >= segStart && + relativeVisualColForHighlight < segEnd + ) { + const charToHighlight = cpSlice( + seg.text, + relativeVisualColForHighlight - segStart, + relativeVisualColForHighlight - segStart + 1, + ); + const highlighted = showCursor + ? chalk.inverse(charToHighlight) + : charToHighlight; + display = + cpSlice( seg.text, + 0, relativeVisualColForHighlight - segStart, + ) + + highlighted + + cpSlice( + seg.text, 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( - - {display} - , - ); - }); - - const currentLineGhost = isOnCursorLine ? inlineGhost : ''; - if ( - isOnCursorLine && - cursorVisualColAbsolute === cpLen(lineText) - ) { - if (!currentLineGhost) { - renderedLine.push( - - {showCursor ? chalk.inverse(' ') : ' '} - , - ); } + charCount = segEnd; } - const showCursorBeforeGhost = - focus && - isOnCursorLine && - cursorVisualColAbsolute === cpLen(lineText) && - currentLineGhost; + const color = + seg.type === 'command' || seg.type === 'file' + ? theme.text.accent + : theme.text.primary; - return ( - - - {renderedLine} - {showCursorBeforeGhost && - (showCursor ? chalk.inverse(' ') : ' ')} - {currentLineGhost && ( - - {currentLineGhost} - - )} - - + renderedLine.push( + + {display} + , ); - }) - .concat( - additionalLines.map((ghostLine, index) => { - const padding = Math.max( - 0, - inputWidth - stringWidth(ghostLine), - ); - return ( - - {ghostLine} - {' '.repeat(padding)} - - ); - }), - ) + }); + + if ( + isOnCursorLine && + cursorVisualColAbsolute === cpLen(lineText) + ) { + renderedLine.push( + + {showCursor ? chalk.inverse(' ') : ' '} + , + ); + } + + return ( + + {renderedLine} + + ); + }) )} diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index bbd18ecf..f96ec33c 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -1271,7 +1271,6 @@ describe('SettingsDialog', () => { vimMode: true, disableAutoUpdate: true, debugKeystrokeLogging: true, - enablePromptCompletion: true, }, ui: { hideWindowTitle: true, @@ -1517,7 +1516,6 @@ describe('SettingsDialog', () => { vimMode: false, disableAutoUpdate: false, debugKeystrokeLogging: false, - enablePromptCompletion: false, }, ui: { hideWindowTitle: false, diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap index b63948e1..cf8d4444 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap @@ -10,8 +10,6 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly │ │ │ Disable Auto Update false │ │ │ -│ Enable Prompt Completion false │ -│ │ │ Debug Keystroke Logging false │ │ │ │ Output Format Text │ @@ -22,6 +20,8 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly │ │ │ Hide Tips false │ │ │ +│ Hide Banner false │ +│ │ │ ▼ │ │ │ │ │ @@ -44,8 +44,6 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select │ │ │ Disable Auto Update false │ │ │ -│ Enable Prompt Completion false │ -│ │ │ Debug Keystroke Logging false │ │ │ │ Output Format Text │ @@ -56,6 +54,8 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select │ │ │ Hide Tips false │ │ │ +│ Hide Banner false │ +│ │ │ ▼ │ │ │ │ │ @@ -78,8 +78,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett │ │ │ Disable Auto Update false │ │ │ -│ Enable Prompt Completion false │ -│ │ │ Debug Keystroke Logging false │ │ │ │ Output Format Text │ @@ -90,6 +88,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett │ │ │ Hide Tips false │ │ │ +│ Hide Banner false │ +│ │ │ ▼ │ │ │ │ │ @@ -112,8 +112,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin │ │ │ Disable Auto Update false* │ │ │ -│ Enable Prompt Completion false* │ -│ │ │ Debug Keystroke Logging false* │ │ │ │ Output Format Text │ @@ -124,6 +122,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin │ │ │ 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 │ │ │ -│ Enable Prompt Completion false │ -│ │ │ Debug Keystroke Logging false │ │ │ │ Output Format Text │ @@ -158,6 +156,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se │ │ │ Hide Tips false │ │ │ +│ Hide Banner false │ +│ │ │ ▼ │ │ │ │ │ @@ -180,8 +180,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se │ │ │ Disable Auto Update false │ │ │ -│ Enable Prompt Completion false │ -│ │ │ Debug Keystroke Logging (Modified in Workspace) false │ │ │ │ Output Format Text │ @@ -192,6 +190,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se │ │ │ Hide Tips false │ │ │ +│ Hide Banner false │ +│ │ │ ▼ │ │ │ │ │ @@ -214,8 +214,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set │ │ │ Disable Auto Update false │ │ │ -│ Enable Prompt Completion false │ -│ │ │ Debug Keystroke Logging false │ │ │ │ Output Format Text │ @@ -226,6 +224,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set │ │ │ Hide Tips false │ │ │ +│ Hide Banner false │ +│ │ │ ▼ │ │ │ │ │ @@ -248,8 +248,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and │ │ │ Disable Auto Update true* │ │ │ -│ Enable Prompt Completion false │ -│ │ │ Debug Keystroke Logging false │ │ │ │ Output Format Text │ @@ -260,6 +258,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and │ │ │ Hide Tips false │ │ │ +│ Hide Banner false │ +│ │ │ ▼ │ │ │ │ │ @@ -282,8 +282,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security │ │ │ Disable Auto Update false │ │ │ -│ Enable Prompt Completion false │ -│ │ │ Debug Keystroke Logging false │ │ │ │ Output Format Text │ @@ -294,6 +292,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security │ │ │ Hide Tips false │ │ │ +│ Hide Banner false │ +│ │ │ ▼ │ │ │ │ │ @@ -316,8 +316,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se │ │ │ Disable Auto Update true* │ │ │ -│ Enable Prompt Completion true* │ -│ │ │ Debug Keystroke Logging true* │ │ │ │ Output Format Text │ @@ -328,6 +326,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se │ │ │ Hide Tips true* │ │ │ +│ Hide Banner false │ +│ │ │ ▼ │ │ │ │ │ diff --git a/packages/cli/src/ui/hooks/useCommandCompletion.test.ts b/packages/cli/src/ui/hooks/useCommandCompletion.test.ts index bf978395..659b99db 100644 --- a/packages/cli/src/ui/hooks/useCommandCompletion.test.ts +++ b/packages/cli/src/ui/hooks/useCommandCompletion.test.ts @@ -83,9 +83,7 @@ const setupMocks = ({ describe('useCommandCompletion', () => { const mockCommandContext = {} as CommandContext; - const mockConfig = { - getEnablePromptCompletion: () => false, - } as Config; + const mockConfig = {} as Config; const testDirs: string[] = []; 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', - ); - }); - }); }); diff --git a/packages/cli/src/ui/hooks/useCommandCompletion.tsx b/packages/cli/src/ui/hooks/useCommandCompletion.tsx index e26bb73d..3deaa8a5 100644 --- a/packages/cli/src/ui/hooks/useCommandCompletion.tsx +++ b/packages/cli/src/ui/hooks/useCommandCompletion.tsx @@ -13,11 +13,6 @@ import { isSlashCommand } from '../utils/commandUtils.js'; import { toCodePoints } from '../utils/textUtils.js'; import { useAtCompletion } from './useAtCompletion.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 { useCompletion } from './useCompletion.js'; @@ -25,7 +20,6 @@ export enum CompletionMode { IDLE = 'IDLE', AT = 'AT', SLASH = 'SLASH', - PROMPT = 'PROMPT', } export interface UseCommandCompletionReturn { @@ -41,7 +35,6 @@ export interface UseCommandCompletionReturn { navigateUp: () => void; navigateDown: () => void; handleAutocomplete: (indexToUse: number) => void; - promptCompletion: PromptCompletion; } 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 { completionMode: CompletionMode.IDLE, query: null, completionStart: -1, completionEnd: -1, }; - }, [cursorRow, cursorCol, buffer.lines, buffer.text, config]); + }, [cursorRow, cursorCol, buffer.lines]); useAtCompletion({ enabled: completionMode === CompletionMode.AT, @@ -172,12 +146,6 @@ export function useCommandCompletion( setIsPerfectMatch, }); - const promptCompletion = usePromptCompletion({ - buffer, - config, - enabled: completionMode === CompletionMode.PROMPT, - }); - useEffect(() => { setActiveSuggestionIndex(suggestions.length > 0 ? 0 : -1); setVisibleStartIndex(0); @@ -264,6 +232,5 @@ export function useCommandCompletion( navigateUp, navigateDown, handleAutocomplete, - promptCompletion, }; } diff --git a/packages/cli/src/ui/hooks/usePromptCompletion.ts b/packages/cli/src/ui/hooks/usePromptCompletion.ts deleted file mode 100644 index 504a22c9..00000000 --- a/packages/cli/src/ui/hooks/usePromptCompletion.ts +++ /dev/null @@ -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(''); - const [isLoadingGhostText, setIsLoadingGhostText] = useState(false); - const abortControllerRef = useRef(null); - const [justSelectedSuggestion, setJustSelectedSuggestion] = - useState(false); - const lastSelectedTextRef = useRef(''); - const lastRequestedTextRef = useRef(''); - - 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, - }; -} diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index c8fa74ab..aa91f785 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -280,7 +280,6 @@ export interface ConfigParameters { skipNextSpeakerCheck?: boolean; shellExecutionConfig?: ShellExecutionConfig; extensionManagement?: boolean; - enablePromptCompletion?: boolean; skipLoopDetection?: boolean; vlmSwitchMode?: string; truncateToolOutputThreshold?: number; @@ -377,7 +376,6 @@ export class Config { private readonly skipNextSpeakerCheck: boolean; private shellExecutionConfig: ShellExecutionConfig; private readonly extensionManagement: boolean = true; - private readonly enablePromptCompletion: boolean = false; private readonly skipLoopDetection: boolean; private readonly skipStartupContext: boolean; private readonly vlmSwitchMode: string | undefined; @@ -495,7 +493,6 @@ export class Config { this.useSmartEdit = params.useSmartEdit ?? false; this.extensionManagement = params.extensionManagement ?? true; this.storage = new Storage(this.targetDir); - this.enablePromptCompletion = params.enablePromptCompletion ?? false; this.vlmSwitchMode = params.vlmSwitchMode; this.fileExclusions = new FileExclusions(this); this.eventEmitter = params.eventEmitter; @@ -1038,10 +1035,6 @@ export class Config { return this.accessibility.screenReader ?? false; } - getEnablePromptCompletion(): boolean { - return this.enablePromptCompletion; - } - getSkipLoopDetection(): boolean { return this.skipLoopDetection; }