2025-08-21 16:04:04 +08:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
import { useState , useCallback , useRef , useEffect , useMemo } from 'react' ;
2026-03-12 02:51:40 +05:30
import {
debugLogger ,
getResponseText ,
LlmRole ,
type Config ,
} from '@google/gemini-cli-core' ;
2025-11-11 08:10:50 -08:00
import type { Content } from '@google/genai' ;
2025-08-26 00:04:53 +02:00
import type { TextBuffer } from '../components/shared/text-buffer.js' ;
2025-08-26 11:51:27 +08:00
import { isSlashCommand } from '../utils/commandUtils.js' ;
2025-08-21 16:04:04 +08:00
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 ;
}
export function usePromptCompletion ( {
buffer ,
config ,
} : 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 > ( '' ) ;
2026-02-22 19:10:20 -05:00
const isPromptCompletionEnabled = false ;
2025-08-21 16:04:04 +08:00
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 ||
2025-08-26 11:51:27 +08:00
isSlashCommand ( trimmedText ) ||
2025-08-21 16:04:04 +08:00
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 response = await geminiClient . generateContent (
2025-11-11 08:10:50 -08:00
{ model : 'prompt-completion' } ,
2025-08-21 16:04:04 +08:00
contents ,
signal ,
2026-02-17 12:32:30 -05:00
LlmRole . UTILITY_AUTOCOMPLETE ,
2025-08-21 16:04:04 +08:00
) ;
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' )
)
) {
2025-10-31 15:39:03 +00:00
debugLogger . warn (
` [WARN] prompt completion failed: : ( ${ error instanceof Error ? error.message : String ( error ) } ) ` ,
) ;
2025-08-21 16:04:04 +08:00
}
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 = '' ;
}
2025-12-05 16:12:49 -08:00
// eslint-disable-next-line @typescript-eslint/no-floating-promises
2025-08-21 16:04:04 +08:00
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 &&
2025-08-26 11:51:27 +08:00
! isSlashCommand ( trimmedText ) &&
2025-08-21 16:04:04 +08:00
! trimmedText . includes ( '@' )
) ;
} , [ buffer . text , isPromptCompletionEnabled , isCursorAtEnd ] ) ;
return {
text : ghostText ,
isLoading : isLoadingGhostText ,
isActive ,
accept : acceptGhostText ,
clear : clearGhostText ,
markSelected : markSuggestionSelected ,
} ;
}