Skip to Content
ДизайнPrompt SuggestionПроектирование подсказок запросов (NES)

Проектирование подсказок запросов (NES)

Предсказывает, что пользователь введет следующим после завершения ответа ИИ, и отображает это в виде ghost text в поле ввода.

Статус реализации: prompt-suggestion-implementation.md. Движок спекулятивного выполнения: speculation-design.md.

Обзор

Подсказка запроса (Next-step Suggestion / NES) — это краткое предсказание (2–12 слов) следующего ввода пользователя, генерируемое вызовом LLM после каждого ответа ИИ. Оно отображается в виде ghost text в поле ввода. Пользователь может принять его с помощью Tab/Enter/Right Arrow или отклонить, начав печатать.

Архитектура

┌─────────────────────────────────────────────────────────────┐ │ AppContainer (CLI) │ │ │ │ Responding → Idle transition │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Guard Conditions (11 categories) │ │ │ │ settings, interactive, sdk, plan mode, dialogs, │ │ │ │ elicitation, API error │ │ │ └────────────────────┬────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ generatePromptSuggestion() │ │ │ │ │ │ │ │ ┌─── CacheSafeParams available? ───┐ │ │ │ │ │ │ │ │ │ │ ▼ YES NO ▼ │ │ │ │ runForkedQuery() BaseLlmClient.generateJson() │ │ │ │ (cache-aware) (standalone fallback) │ │ │ │ │ │ │ │ ──── SUGGESTION_PROMPT ──── │ │ │ │ ──── 12 filter rules ────── │ │ │ │ ──── getFilterReason() ──── │ │ │ └────────────────────┬────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ FollowupController (framework-agnostic) │ │ │ │ 300ms delay → show as ghost text │ │ │ │ │ │ │ │ Tab → accept (fill input) │ │ │ │ Enter → accept + submit │ │ │ │ Right → accept (fill input) │ │ │ │ Type → dismiss + abort speculation │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Telemetry (PromptSuggestionEvent) │ │ │ │ outcome, accept_method, timing, similarity, │ │ │ │ keystroke, focus, suppression reason, prompt_id │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘

Генерация подсказок

Промпт для LLM

[SUGGESTION MODE: Suggest what the user might naturally type next.] FIRST: Read the LAST FEW LINES of the assistant's most recent message — that's where next-step hints, tips, and actionable suggestions usually appear. Then check the user's recent messages and original request. Your job is to predict what THEY would type - not what you think they should do. THE TEST: Would they think "I was just about to type that"? PRIORITY: If the assistant's last message contains a tip or hint like "Tip: type X to ..." or "type X to ...", extract X as the suggestion. These are explicit next-step hints. EXAMPLES: Assistant says "Tip: type post comments to publish findings" → "post comments" Assistant says "type /review to start" → "/review" User asked "fix the bug and run tests", bug is fixed → "run the tests" After code written → "try it out" Task complete, obvious follow-up → "commit this" or "push it" Format: 2-12 words, match the user's style. Or nothing. Reply with ONLY the suggestion, no quotes or explanation.

Правила фильтрации (12)

ПравилоПример блокировки
done”done”
meta_text”nothing found”, “no suggestion”, “silence”
meta_wrapped”(silence)”, “[no suggestion]“
error_message”api error: 500”
prefixed_label”Suggestion: commit”
too_few_words”hmm” (но разрешает “yes”, “commit”, “push” и т.д.)
too_many_words> 12 слов
too_long>= 100 символов
multiple_sentences”Run tests. Then commit.”
has_formattingпереносы строк, жирный текст markdown
evaluative”looks good”, “thanks” (с границами слов \b)
ai_voice”Let me…”, “I’ll…”, “Here’s…”

Guard Conditions

AppContainer useEffect (13 проверок в коде):

УсловиеПроверка
Переключатель настроекenableFollowupSuggestions
Неинтерактивный режимconfig.isInteractive()
Режим SDK!config.getSdkMode()
Переход потоковой передачиResponding → Idle (2 проверки)
Ошибка API (история)historyManager.history[last]?.type !== 'error'
Ошибка API (ожидание)!pendingGeminiHistoryItems.some(type === 'error')
Диалоги подтвержденияshell + general + loop detection (3 проверки)
Диалог разрешенийisPermissionsDialogOpen
Запрос уточненийsettingInputRequests.length === 0
Режим планированияApprovalMode.PLAN

Внутри generatePromptSuggestion():

УсловиеПроверка
Начало диалогаmodelTurns < 2

Отдельные feature flags (не в блоке guard):

ФлагУправление
enableCacheSharingИспользовать ли forked-запрос или fallback на generateJson
enableSpeculationЗапускать ли спекулятивное выполнение при отображении подсказки

Управление состоянием

FollowupState

interface FollowupState { suggestion: string | null; isVisible: boolean; shownAt: number; // timestamp for telemetry }

FollowupController

Фреймворк-агностичный контроллер, общий для CLI (Ink) и WebUI (React):

  • setSuggestion(text) — отложенный показ на 300 мс, null очищает немедленно
  • accept(method) — очищает состояние, вызывает onAccept через микротаск, блокировка debounce на 100 мс
  • dismiss() — очищает состояние, логирует телеметрию ignored
  • clear() — полный сброс состояния и таймеров
  • Object.freeze(INITIAL_FOLLOWUP_STATE) предотвращает случайную мутацию

Взаимодействие с клавиатурой

КлавишаCLIWebUI
TabЗаполняет ввод (без отправки)Заполняет ввод (без отправки)
EnterЗаполняет + отправляетЗаполняет + отправляет (explicitText param)
Right ArrowЗаполняет ввод (без отправки)Заполняет ввод (без отправки)
Ввод текстаОтклоняет + прерывает спекуляциюОтклоняет
ВставкаОтклоняет + прерывает спекуляциюОтклоняет

Примечание по привязке клавиш

Обработчик Tab явно использует key.name === 'tab' (а не матчер ACCEPT_SUGGESTION), потому что ACCEPT_SUGGESTION также совпадает с Enter, который должен передаваться дальше в обработчик SUBMIT.

Телеметрия

PromptSuggestionEvent

ПолеТипОписание
outcomeaccepted/ignored/suppressedИтоговый результат
prompt_idstringПо умолчанию: ‘user_intent’
accept_methodtab/enter/rightСпособ принятия пользователем
time_to_accept_msnumberВремя от показа до принятия
time_to_ignore_msnumberВремя от показа до отклонения
time_to_first_keystroke_msnumberВремя до первого нажатия клавиши во время показа
suggestion_lengthnumberКоличество символов
similaritynumber1.0 при принятии, 0.0 при отклонении
was_focused_when_shownbooleanТерминал был в фокусе
reasonstringДля suppressed: имя правила фильтрации

SpeculationEvent

ПолеТипОписание
outcomeaccepted/aborted/failedРезультат спекуляции
turns_usednumberКоличество API round-trips
files_writtennumberФайлы в оверлее
tool_use_countnumberВыполненные инструменты
duration_msnumberРеальное время выполнения
boundary_typestringЧто остановило спекуляцию
had_pipelined_suggestionbooleanСледующая подсказка сгенерирована

Feature Flags и настройки

НастройкаТипПо умолчаниюОписание
enableFollowupSuggestionsbooleantrueГлавный переключатель подсказок запросов
enableCacheSharingbooleantrueИспользовать кэш-ориентированные forked-запросы
enableSpeculationbooleanfalseДвижок предсказательного выполнения
fastModel (верхний уровень)string""Модель для всех фоновых задач (пусто = использовать основную модель). Устанавливается через /model --fast

Фильтрация внутренних Prompt ID

Фоновые операции используют выделенные prompt ID (INTERNAL_PROMPT_IDS в utils/internalPromptIds.ts), чтобы их API-трафик и вызовы инструментов не отображались в пользовательском интерфейсе:

Prompt IDИспользуется
prompt_suggestionГенерация подсказок
forked_queryКэш-ориентированные forked-запросы
speculationДвижок спекулятивного выполнения

Применяемая фильтрация:

  • loggingContentGenerator — пропускает logApiRequest и логирование взаимодействия с OpenAI для внутренних ID
  • logApiResponse / logApiError — пропускает chatRecordingService.recordUiTelemetryEvent
  • logToolCall — пропускает chatRecordingService.recordUiTelemetryEvent
  • uiTelemetryService.addEventне фильтруется (обеспечивает работу отслеживания токенов в /stats)

Thinking Mode

Режим мышления/рассуждений явно отключен (thinkingConfig: { includeThoughts: false }) для всех путей фоновых задач:

  • Путь forked-запроса (createForkedChat) — переопределяет thinkingConfig в клонированном generationConfig, охватывая как генерацию подсказок, так и спекуляцию
  • Fallback-путь BaseLlm (generateViaBaseLlm) — конфигурация на каждый запрос переопределяет настройки мышления базового генератора контента

Это безопасно, потому что:

  • Префикс кэша определяется systemInstruction + tools + history, а не thinkingConfig — попадания в кэш не затрагиваются
  • Все бэкенды (Gemini, OpenAI-compatible, Anthropic) обрабатывают includeThoughts: false, опуская поле мышления — ошибок API на моделях без поддержки мышления не возникает
  • Генерация подсказок и спекуляция не получают преимуществ от токенов рассуждений
Last updated on