From 589f5e6823eca456d9c93cadea664e5c19eebb90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=98=E4=BD=99=E6=A3=9A?= <1@linux.com> Date: Thu, 21 Aug 2025 16:04:04 +0800 Subject: [PATCH] feat(cli): prompt completion (#4691) Co-authored-by: Jacob Richman --- packages/cli/src/config/config.ts | 1 + packages/cli/src/config/settingsSchema.ts | 10 + packages/cli/src/ui/App.test.tsx | 1 + .../src/ui/components/InputPrompt.test.tsx | 5 + .../cli/src/ui/components/InputPrompt.tsx | 249 ++++++++++++++--- .../src/ui/hooks/useCommandCompletion.test.ts | 6 +- .../cli/src/ui/hooks/useCommandCompletion.tsx | 49 +++- .../cli/src/ui/hooks/usePromptCompletion.ts | 253 ++++++++++++++++++ packages/core/index.ts | 1 + packages/core/src/config/config.ts | 7 + packages/core/src/index.ts | 1 + 11 files changed, 540 insertions(+), 43 deletions(-) create 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 9731b503..0b21ff2e 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -542,6 +542,7 @@ export async function loadCliConfig( trustedFolder, shouldUseNodePtyShell: settings.shouldUseNodePtyShell, skipNextSpeakerCheck: settings.skipNextSpeakerCheck, + enablePromptCompletion: settings.enablePromptCompletion ?? false, }); } diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 6d9e1f1e..7f28b698 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -524,6 +524,16 @@ export const SETTINGS_SCHEMA = { description: 'Skip the next speaker check.', showInDialog: true, }, + enablePromptCompletion: { + type: 'boolean', + label: 'Enable Prompt Completion', + category: 'General', + requiresRestart: true, + default: false, + description: + 'Enable AI-powered prompt completion suggestions while typing.', + showInDialog: true, + }, } as const; type InferSettings = { diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index f78ec580..9f8a681f 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -147,6 +147,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { getShowMemoryUsage: vi.fn(() => opts.showMemoryUsage ?? false), getAccessibility: vi.fn(() => opts.accessibility ?? {}), getProjectRoot: vi.fn(() => opts.targetDir), + getEnablePromptCompletion: vi.fn(() => false), getGeminiClient: vi.fn(() => ({ getUserTier: vi.fn(), })), diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 11a0eb48..a3346490 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -160,6 +160,11 @@ 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 02c25bd8..01666c66 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -10,7 +10,7 @@ import { theme } from '../semantic-colors.js'; import { SuggestionsDisplay } from './SuggestionsDisplay.js'; import { useInputHistory } from '../hooks/useInputHistory.js'; import { TextBuffer, logicalPosToOffset } from './shared/text-buffer.js'; -import { cpSlice, cpLen } from '../utils/textUtils.js'; +import { cpSlice, cpLen, toCodePoints } from '../utils/textUtils.js'; import chalk from 'chalk'; import stringWidth from 'string-width'; import { useShellHistory } from '../hooks/useShellHistory.js'; @@ -403,6 +403,16 @@ 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.HISTORY_UP](key)) { inputHistory.navigateUp(); @@ -507,6 +517,17 @@ 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(); + } }, [ focus, @@ -540,6 +561,119 @@ 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(); + return ( <> = ({ {placeholder} ) ) : ( - linesToRender.map((lineText, visualIdxInRenderedSet) => { - const cursorVisualRow = cursorVisualRowAbsolute - scrollVisualRow; - let display = cpSlice(lineText, 0, inputWidth); - const currentVisualWidth = stringWidth(display); - if (currentVisualWidth < inputWidth) { - display = display + ' '.repeat(inputWidth - currentVisualWidth); - } + linesToRender + .map((lineText, visualIdxInRenderedSet) => { + const cursorVisualRow = + cursorVisualRowAbsolute - scrollVisualRow; + let display = cpSlice(lineText, 0, inputWidth); - if (focus && visualIdxInRenderedSet === cursorVisualRow) { - const relativeVisualColForHighlight = cursorVisualColAbsolute; + const isOnCursorLine = + focus && visualIdxInRenderedSet === cursorVisualRow; + const currentLineGhost = isOnCursorLine ? inlineGhost : ''; - if (relativeVisualColForHighlight >= 0) { - if (relativeVisualColForHighlight < cpLen(display)) { - const charToHighlight = - cpSlice( - display, - relativeVisualColForHighlight, - relativeVisualColForHighlight + 1, - ) || ' '; - const highlighted = chalk.inverse(charToHighlight); - display = - cpSlice(display, 0, relativeVisualColForHighlight) + - highlighted + - cpSlice(display, relativeVisualColForHighlight + 1); - } else if ( - relativeVisualColForHighlight === cpLen(display) && - cpLen(display) === inputWidth - ) { - display = display + chalk.inverse(' '); + const ghostWidth = stringWidth(currentLineGhost); + + if (focus && visualIdxInRenderedSet === cursorVisualRow) { + const relativeVisualColForHighlight = cursorVisualColAbsolute; + + if (relativeVisualColForHighlight >= 0) { + if (relativeVisualColForHighlight < cpLen(display)) { + const charToHighlight = + cpSlice( + display, + relativeVisualColForHighlight, + relativeVisualColForHighlight + 1, + ) || ' '; + const highlighted = chalk.inverse(charToHighlight); + display = + cpSlice(display, 0, relativeVisualColForHighlight) + + highlighted + + cpSlice(display, relativeVisualColForHighlight + 1); + } else if ( + relativeVisualColForHighlight === cpLen(display) + ) { + if (!currentLineGhost) { + display = display + chalk.inverse(' '); + } + } } } - } - return ( - {display} - ); - }) + + const showCursorBeforeGhost = + focus && + visualIdxInRenderedSet === cursorVisualRow && + cursorVisualColAbsolute === + // eslint-disable-next-line no-control-regex + cpLen(display.replace(/\x1b\[[0-9;]*m/g, '')) && + currentLineGhost; + + const actualDisplayWidth = stringWidth(display); + const cursorWidth = showCursorBeforeGhost ? 1 : 0; + const totalContentWidth = + actualDisplayWidth + cursorWidth + ghostWidth; + const trailingPadding = Math.max( + 0, + inputWidth - totalContentWidth, + ); + + return ( + + {display} + {showCursorBeforeGhost && chalk.inverse(' ')} + {currentLineGhost && ( + + {currentLineGhost} + + )} + {trailingPadding > 0 && ' '.repeat(trailingPadding)} + + ); + }) + .concat( + additionalLines.map((ghostLine, index) => { + const padding = Math.max( + 0, + inputWidth - stringWidth(ghostLine), + ); + return ( + + {ghostLine} + {' '.repeat(padding)} + + ); + }), + ) )} diff --git a/packages/cli/src/ui/hooks/useCommandCompletion.test.ts b/packages/cli/src/ui/hooks/useCommandCompletion.test.ts index a3c96935..00bc8ac3 100644 --- a/packages/cli/src/ui/hooks/useCommandCompletion.test.ts +++ b/packages/cli/src/ui/hooks/useCommandCompletion.test.ts @@ -84,7 +84,9 @@ const setupMocks = ({ describe('useCommandCompletion', () => { const mockCommandContext = {} as CommandContext; - const mockConfig = {} as Config; + const mockConfig = { + getEnablePromptCompletion: () => false, + } as Config; const testDirs: string[] = []; const testRootDir = '/'; @@ -511,7 +513,7 @@ describe('useCommandCompletion', () => { }); expect(result.current.textBuffer.text).toBe( - '@src/file1.txt is a good file', + '@src/file1.txt is a good file', ); }); }); diff --git a/packages/cli/src/ui/hooks/useCommandCompletion.tsx b/packages/cli/src/ui/hooks/useCommandCompletion.tsx index 07d0e056..166f03c5 100644 --- a/packages/cli/src/ui/hooks/useCommandCompletion.tsx +++ b/packages/cli/src/ui/hooks/useCommandCompletion.tsx @@ -15,6 +15,11 @@ import { isSlashCommand } from '../utils/commandUtils.js'; import { toCodePoints } from '../utils/textUtils.js'; import { useAtCompletion } from './useAtCompletion.js'; import { useSlashCompletion } from './useSlashCompletion.js'; +import { + usePromptCompletion, + PromptCompletion, + PROMPT_COMPLETION_MIN_LENGTH, +} from './usePromptCompletion.js'; import { Config } from '@google/gemini-cli-core'; import { useCompletion } from './useCompletion.js'; @@ -22,6 +27,7 @@ export enum CompletionMode { IDLE = 'IDLE', AT = 'AT', SLASH = 'SLASH', + PROMPT = 'PROMPT', } export interface UseCommandCompletionReturn { @@ -37,6 +43,7 @@ export interface UseCommandCompletionReturn { navigateUp: () => void; navigateDown: () => void; handleAutocomplete: (indexToUse: number) => void; + promptCompletion: PromptCompletion; } export function useCommandCompletion( @@ -93,12 +100,7 @@ export function useCommandCompletion( backslashCount++; } if (backslashCount % 2 === 0) { - return { - completionMode: CompletionMode.IDLE, - query: null, - completionStart: -1, - completionEnd: -1, - }; + break; } } else if (char === '@') { let end = codePoints.length; @@ -125,13 +127,33 @@ 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 && + !trimmedText.startsWith('/') && + !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]); + }, [cursorRow, cursorCol, buffer.lines, buffer.text, config]); useAtCompletion({ enabled: completionMode === CompletionMode.AT, @@ -152,6 +174,12 @@ export function useCommandCompletion( setIsPerfectMatch, }); + const promptCompletion = usePromptCompletion({ + buffer, + config, + enabled: completionMode === CompletionMode.PROMPT, + }); + useEffect(() => { setActiveSuggestionIndex(suggestions.length > 0 ? 0 : -1); setVisibleStartIndex(0); @@ -202,7 +230,11 @@ export function useCommandCompletion( } } - suggestionText += ' '; + const lineCodePoints = toCodePoints(buffer.lines[cursorRow] || ''); + const charAfterCompletion = lineCodePoints[end]; + if (charAfterCompletion !== ' ') { + suggestionText += ' '; + } buffer.replaceRangeByOffset( logicalPosToOffset(buffer.lines, cursorRow, start), @@ -234,5 +266,6 @@ 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 new file mode 100644 index 00000000..466d020b --- /dev/null +++ b/packages/cli/src/ui/hooks/usePromptCompletion.ts @@ -0,0 +1,253 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; +import { + Config, + DEFAULT_GEMINI_FLASH_LITE_MODEL, + getResponseText, +} from '@google/gemini-cli-core'; +import { Content, GenerateContentConfig } from '@google/genai'; +import { TextBuffer } from '../components/shared/text-buffer.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 || + trimmedText.startsWith('/') || + 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 && + !trimmedText.startsWith('/') && + !trimmedText.includes('@') + ); + }, [buffer.text, isPromptCompletionEnabled, isCursorAtEnd]); + + return { + text: ghostText, + isLoading: isLoadingGhostText, + isActive, + accept: acceptGhostText, + clear: clearGhostText, + markSelected: markSuggestionSelected, + }; +} diff --git a/packages/core/index.ts b/packages/core/index.ts index 7b75b365..660306ed 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -8,6 +8,7 @@ export * from './src/index.js'; export { DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_FLASH_MODEL, + DEFAULT_GEMINI_FLASH_LITE_MODEL, DEFAULT_GEMINI_EMBEDDING_MODEL, } from './src/config/models.js'; export { logIdeConnection } from './src/telemetry/loggers.js'; diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 349a0f83..44df13a8 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -200,6 +200,7 @@ export interface ConfigParameters { trustedFolder?: boolean; shouldUseNodePtyShell?: boolean; skipNextSpeakerCheck?: boolean; + enablePromptCompletion?: boolean; } export class Config { @@ -267,6 +268,7 @@ export class Config { private readonly trustedFolder: boolean | undefined; private readonly shouldUseNodePtyShell: boolean; private readonly skipNextSpeakerCheck: boolean; + private readonly enablePromptCompletion: boolean = false; private initialized: boolean = false; readonly storage: Storage; @@ -338,6 +340,7 @@ export class Config { this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false; this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? false; this.storage = new Storage(this.targetDir); + this.enablePromptCompletion = params.enablePromptCompletion ?? false; if (params.contextFileName) { setGeminiMdFilename(params.contextFileName); @@ -731,6 +734,10 @@ export class Config { return this.skipNextSpeakerCheck; } + getEnablePromptCompletion(): boolean { + return this.enablePromptCompletion; + } + async getGitService(): Promise { if (!this.gitService) { this.gitService = new GitService(this.targetDir, this.storage); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f8cd08a8..afdba8fc 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -41,6 +41,7 @@ export * from './utils/shell-utils.js'; export * from './utils/systemEncoding.js'; export * from './utils/textUtils.js'; export * from './utils/formatters.js'; +export * from './utils/generateContentResponseUtilities.js'; export * from './utils/filesearch/fileSearch.js'; export * from './utils/errorParsing.js';