Фаза 2 — Технический проект: Расширение возможностей
1. Цели проектирования и ограничения
1.1 Цели
- Расширить
supportedModesдля 13 встроенных команд, чтобы включитьnon_interactiveи/илиacp - Обеспечить, чтобы каждая расширенная команда в режимах ACP/non-interactive возвращала текстовое содержимое, пригодное для потребления IDE
- Реализовать канал вызова модели для prompt command (
SkillToolиспользуетgetModelInvocableCommands()) - Реализовать базовое обнаружение slash command в середине ввода
1.2 Жёсткие ограничения
- Нулевая деградация интерактивного пути: существующее интерактивное поведение всех расширяемых команд строго сохраняется — изменения вносятся только внутри
actionпутём добавления ветвления по режиму, интерактивный путь не затрагивается - Стратегия реализации: ветвление по режиму, а не двойная регистрация: все 13 команд используют проверку
executionModeвнутриaction, без применения шаблона двойной регистрации, описанного в §10.2 документа по фазе 1 (двойная регистрация оправдана только при кардинально различающейся логике интерактивного и неинтерактивного режимов, что не соответствует сложности текущих команд) - Формат сообщений ACP: текстовое содержимое, возвращаемое по ACP-пути, не должно содержать ANSI-стилей и должно быть в Markdown или plain text, ориентировано на потребление плагином IDE
- Пропуск побочных эффектов, зависящих от окружения: операции, зависящие от графического окружения, такие как открытие браузера (
open()), работа с буфером обмена (copyToClipboard()), должны быть пропущены в режимах non-interactive/ACP
2. Базовое состояние после завершения Фазы 1
Ключевые моменты архитектуры после Фазы 1 (на них напрямую строится Фаза 2):
- Поле
commandTypeудалено из интерфейсаSlashCommand; все команды теперь используют явныйsupportedModes getEffectiveSupportedModes()— двухуровневый вывод: явныйsupportedModes→ резервныйCommandKindCommandService.getCommandsForMode(mode)заменил белый списокALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVEbtw,bug,compress,context,init,summaryуже расширены на все режимы в Фазе 1 и не входят в список текущей фазы- Все методы
createNonInteractiveUI()являются no-op:addItem,clear,setDebugMessage,setPendingItem,reloadCommands— все вызовы молча игнорируются
3. Общий объём изменений
В данной фазе задействовано 13 команд, разделённых по сложности реализации на четыре категории:
| Категория | Команды | Основные изменения |
|---|---|---|
| Категория А | export | Только изменение supportedModes; все пути в action уже возвращают корректные типы |
| Только инт. | plan, statusline | Дизайн-решение: эти команды семантически тесно связаны с интерактивным интерфейсом; оставляем supportedModes: ['interactive'] |
| Категория А+ | language | Изменение supportedModes + небольшая обработка non-interactive ветки |
| Только инт. | copy, restore | Дизайн-решение: буфер обмена и восстановление снимков — по сути интерактивные операции; оставляем supportedModes: ['interactive'] |
| Категория А’ | model, approval-mode | Пути с параметрами уже возвращают message; пути без параметров требуют новой non-interactive ветки (сейчас вызывают dialog) |
| Категория B | about, stats, insight, docs, clear | Все пути в action не возвращают значение или вызывают addItem/clear; требуется полная новая non-interactive ветка |
4. Категория A: Только изменение supportedModes
Во всех путях action этих команд уже возвращается message или submit_prompt, полностью без UI-зависимостей; handleCommandResult может обработать их напрямую.
4.1 /export (и подкоманды)
Текущее состояние: supportedModes: ['interactive']; все подкоманды возвращают MessageActionReturn.
Изменение: Изменить supportedModes родительской команды и всех четырёх подкоманд (md, html, json, jsonl) на ['interactive', 'non_interactive', 'acp'].
Содержимое ACP-сообщения: существующее возвращаемое содержимое action уже содержит полный путь к файлу (например, Session exported to markdown: qwen-export-2024-01-01T12-00-00.md), что удобно для использования в IDE; изменять текст не требуется.
Примечание: Родительская команда
/exportсама не имеетaction, только подкоманды. После измененияsupportedModesродителя на все режимыparseSlashCommandсможет сопоставлять маршруты подкоманд, но если пользователь введёт только/exportбез подкоманды,commandToExecute.actionбудет undefined,handleSlashCommandвернётno_command, и вызывающая сторона покажет подсказку с доступными подкомандами. Это ожидаемое поведение.
4.2 /plan
Текущее состояние: supportedModes: ['interactive']; все пути action возвращают MessageActionReturn или SubmitPromptActionReturn.
Дизайн-решение: /plan — команда для направления пользователя к многошаговому интерактивному планированию; семантически тесно связана с интерактивным интерфейсом. После обсуждения принято решение оставить supportedModes: ['interactive'], не расширять на non-interactive/acp.
4.3 /statusline
Текущее состояние: supportedModes: ['interactive']; action всегда возвращает SubmitPromptActionReturn (отправляет модели prompt для вызова subagent).
Дизайн-решение: /statusline — команда для запуска subagent для обобщения текущего состояния; семантически тесно связана с интерактивным интерфейсом. После обсуждения принято решение оставить supportedModes: ['interactive'], не расширять на non-interactive/acp.
5. Категория A+: Небольшая обработка non-interactive ветки
5.1 /language
Текущее состояние: Все пути action возвращают MessageActionReturn (чтение/установка языковых настроек).
Побочные эффекты, требующие обработки: setUiLanguage() вызывает context.ui.reloadCommands(), который в неинтерактивном UI уже является no-op; дополнительной обработки не требуется.
Изменение:
- Изменить
supportedModesродительской команды и подкоманд (ui,output, а также динамически генерируемых подкоманд изSUPPORTED_LANGUAGES) на['interactive', 'non_interactive', 'acp']. - В action не требуется добавлять ветвление по режиму; существующий возвращаемый текст уже пригоден для машинного потребления.
Семантика ACP: В non-interactive (однократный вызов) выполнение /language ui zh-CN изменит постоянные настройки (запись в файл settings); изменения вступят в силу для последующих сессий, а также i18n в текущей сессии применится немедленно. Это соответствует ожиданиям пользователя.
5.2 /copy
Текущее состояние: Action вызывает copyToClipboard(), что в среде ACP/headless может выбросить исключение или молча завершиться неудачей (буфер обмена недоступен).
Изменение:
- Изменить
supportedModesна['interactive', 'non_interactive', 'acp']. - Добавить новую ветвь по режиму внутри action:
// Получение последнего AI-сообщения (существующая логика, можно переиспользовать)
if (context.executionMode !== 'interactive') {
// Неинтерактивный/ACP: пропустить буфер обмена, вернуть само содержимое
if (!lastAiOutput) {
return {
type: 'message',
messageType: 'info',
content: 'No output in history.',
};
}
return {
type: 'message',
messageType: 'info',
content: lastAiOutput,
};
}
// Интерактивный путь: исходная логика с буфером обмена без изменений
await copyToClipboard(lastAiOutput);
return {
type: 'message',
messageType: 'info',
content: 'Last output copied to the clipboard',
};Семантика ACP: IDE получает исходный текст последнего вывода модели; может самостоятельно решить, копировать ли его в буфер обмена или показать пользователю.
5.3 /restore
Текущее состояние: supportedModes: ['interactive'].
Дизайн-решение: Восстановление снимка в дальнейшем приводит к повторному выполнению вызовов инструментов; семантически тесно связано с интерактивным интерфейсом. После обсуждения принято решение оставить supportedModes: ['interactive'], не расширять на non-interactive/acp.
Семантика ACP: Восстановление git-состояния checkpoint и установка истории gemini client выполняются как побочные эффекты; IDE после получения подтверждающего сообщения может уведомить пользователя «Состояние восстановлено». Повторное выполнение инструментов IDE решает, запускать ли самостоятельно.
6. Категория A’: Обработка неинтерактивного пути для диалогов без параметров
6.1 /model
Текущее состояние:
| Ввод | Текущее поведение |
|---|---|
/model (без параметров) | → { type: 'dialog', dialog: 'model' } (в non-interactive становится unsupported) |
/model <model-id> | Не реализовано (есть только ветка --fast) |
/model --fast (без имени модели) | → { type: 'dialog', dialog: 'fast-model' } (в non-interactive становится unsupported) |
/model --fast <model-id> | → MessageActionReturn ✅ |
Изменение:
- Изменить
supportedModesна['interactive', 'non_interactive', 'acp']. - Вставить ветвь non-interactive перед каждым путём, возвращающим dialog:
// Путь без параметров (изначально возвращает dialog: 'model')
if (!args.trim()) {
if (context.executionMode !== 'interactive') {
const currentModel = config.getModel() ?? 'unknown';
return {
type: 'message',
messageType: 'info',
content: `Current model: ${currentModel}\nUse "/model <model-id>" to switch models.`,
};
}
return { type: 'dialog', dialog: 'model' };
}
// Путь --fast без параметров (изначально возвращает dialog: 'fast-model')
if (args.startsWith('--fast') && !modelName) {
if (context.executionMode !== 'interactive') {
const fastModel = context.services.settings?.merged?.fastModel ?? 'not set';
return {
type: 'message',
messageType: 'info',
content: `Current fast model: ${fastModel}\nUse "/model --fast <model-id>" to set fast model.`,
};
}
return { type: 'dialog', dialog: 'fast-model' };
}Семантика ACP: IDE отображает имя текущей модели для справки пользователя; переключение модели осуществляется вызовом с параметром (/model <model-id>).
Примечание: В
/model <model-id>(без--fast) в настоящее время не реализована логика установки модели текущей сессии — она есть только для--fast <model-id>. Если Фаза 2 должна поддерживать переключение основной модели в ACP, необходимо одновременно реализовать логику set для/model <model-id>. Данный проект оставляет этот путь зарезервированным, но помечает его как опцию Фазы 2; приоритет — обеспечить read-only путь «просмотр текущей модели».
6.2 /approval-mode
Текущее состояние:
| Ввод | Текущее поведение |
|---|---|
/approval-mode (без параметров) | → { type: 'dialog', dialog: 'approval-mode' } (в non-interactive становится unsupported) |
/approval-mode <mode> | → MessageActionReturn ✅ |
/approval-mode <invalid> | → MessageActionReturn (ошибка) ✅ |
Изменение:
- Изменить
supportedModesна['interactive', 'non_interactive', 'acp']. - Вставить ветвь non-interactive для пути без параметров (
!args.trim()):
if (!args.trim()) {
if (context.executionMode !== 'interactive') {
const currentMode = config?.getApprovalMode() ?? 'unknown';
return {
type: 'message',
messageType: 'info',
content: `Current approval mode: ${currentMode}\nAvailable modes: ${APPROVAL_MODES.join(', ')}\nUse "/approval-mode <mode>" to change.`,
};
}
return { type: 'dialog', dialog: 'approval-mode' };
}7. Категория B: Требуется полная non-interactive ветка
У этих пяти команд в интерактивном режиме action визуализирует React-компоненты через context.ui.addItem() или вызывает context.ui.clear(); возвращаемое значение — void. В non-interactive эти вызовы являются no-op, поэтому handleSlashCommand обработает отсутствие возвращаемого значения как "Command executed successfully.", без фактического вывода содержимого.
Принцип реализации: Проверить executionMode в начале action; при неинтерактивном режиме досрочно вернуть message с фактическим содержимым; интерактивный путь не трогать.
7.1 /about (altName: status)
Источник данных: getExtendedSystemInfo(context) возвращает ExtendedSystemInfo, содержащий: cliVersion, osPlatform, osArch, osRelease, nodeVersion, modelVersion, selectedAuthType, ideClient, sessionId, memoryUsage, baseUrl, apiKeyEnvKey, gitCommit, fastModel. Все поля доступны в non-interactive (context.services.config и settings уже внедрены).
Изменение:
- Изменить
supportedModesна['interactive', 'non_interactive', 'acp']. - После вызова
getExtendedSystemInfo, перед интерактивным путём, вставить ветвь по режиму:
action: async (context) => {
const systemInfo = await getExtendedSystemInfo(context);
if (context.executionMode !== 'interactive') {
const lines = [
`Qwen Code v${systemInfo.cliVersion}`,
`Model: ${systemInfo.modelVersion}`,
`Fast Model: ${systemInfo.fastModel ?? 'not set'}`,
`Auth: ${systemInfo.selectedAuthType}`,
`Platform: ${systemInfo.osPlatform} ${systemInfo.osArch} (${systemInfo.osRelease})`,
`Node.js: ${systemInfo.nodeVersion}`,
`Session: ${systemInfo.sessionId}`,
...(systemInfo.gitCommit ? [`Git commit: ${systemInfo.gitCommit}`] : []),
...(systemInfo.ideClient ? [`IDE: ${systemInfo.ideClient}`] : []),
];
return {
type: 'message',
messageType: 'info',
content: lines.join('\n'),
};
}
// Интерактивный путь: исходная логика addItem без изменений
const aboutItem: Omit<HistoryItemAbout, 'id'> = { type: MessageType.ABOUT, systemInfo };
context.ui.addItem(aboutItem, Date.now());
},7.2 /stats (и подкоманды model, tools)
Источник данных: context.session.stats (SessionStatsState) содержит sessionStartTime, metrics (SessionMetrics: models, tools, files), promptCount. В non-interactive sessionStartTime — это момент текущего вызова; metrics берутся из uiTelemetryService.getMetrics() (значения, накопленные за текущий вызов, обычно нулевые); promptCount равен 1.
Изменение:
- Изменить
supportedModesродительской командыstatsи подкомандmodel,toolsна['interactive', 'non_interactive', 'acp']. - Вставить ветвь по режиму в action родительской команды и каждой подкоманды с досрочным возвратом статистики в текстовом формате:
// Основная команда /stats
action: (context) => {
if (context.executionMode !== 'interactive') {
const now = new Date();
const { sessionStartTime, promptCount, metrics } = context.session.stats;
if (!sessionStartTime) {
return { type: 'message', messageType: 'error', content: 'Session start time unavailable.' };
}
const wallDuration = now.getTime() - sessionStartTime.getTime();
// Суммировать токены по всем моделям
let totalPromptTokens = 0, totalCandidateTokens = 0, totalRequests = 0;
for (const modelMetrics of Object.values(metrics.models)) {
totalPromptTokens += modelMetrics.tokens.prompt;
totalCandidateTokens += modelMetrics.tokens.candidates;
totalRequests += modelMetrics.api.totalRequests;
}
const lines = [
`Session duration: ${formatDuration(wallDuration)}`,
`Prompts: ${promptCount}`,
`API requests: ${totalRequests}`,
`Tokens — prompt: ${totalPromptTokens}, output: ${totalCandidateTokens}`,
`Tool calls: ${metrics.tools.totalCalls} (${metrics.tools.totalSuccess} ok, ${metrics.tools.totalFail} fail)`,
`Files: +${metrics.files.totalLinesAdded} / -${metrics.files.totalLinesRemoved} lines`,
];
return { type: 'message', messageType: 'info', content: lines.join('\n') };
}
// Интерактивный путь: исходная логика addItem без изменений
const statsItem: HistoryItemStats = { type: MessageType.STATS, duration: formatDuration(wallDuration) };
context.ui.addItem(statsItem, Date.now());
},Подкоманды model и tools также вставляют свои ветви по режиму, возвращая текстовую статистику по соответствующему измерению (model: по имени модели с указанием использованных токенов; tools: количество вызовов каждого инструмента).
Пояснение: При однократном вызове в non-interactive метрики обычно нулевые (новая сессия), но структура полна, что не влияет на формат. В ACP-сессии могут быть накопленные значения, имеющие смысл.
7.3 /insight
Текущее состояние: Action возвращает void, отображая прогресс и результат через addItem, затем вызывает open(outputPath) для открытия браузера. Основная логика — insightGenerator.generateStaticInsight() для генерации HTML-файла.
Изменение:
- Изменить
supportedModesна['interactive', 'non_interactive', 'acp']. - Разветвление по
executionModeна три пути:non_interactive: синхронная генерация, без колбэков прогресса, без открытия браузера, напрямую возвращаетmessage(путь к файлу)acp: асинхронный запуск генерации, передача прогресса черезstream_messages(encodeInsightProgressMessage) и завершение (encodeInsightReadyMessage) в IDEinteractive: исходная логикаaddItem+setPendingItem+open()без изменений
// non_interactive путь
if (context.executionMode === 'non_interactive') {
const outputPath = await insightGenerator.generateStaticInsight(
projectsDir,
() => {}, // no-op progress
);
return {
type: 'message',
messageType: 'info',
content: t('Insight report generated at: {{path}}', { path: outputPath }),
};
}
// acp путь: stream_messages
if (context.executionMode === 'acp') {
// ... построить async generator streamMessages, yield encodeInsightProgressMessage / encodeInsightReadyMessage ...
return { type: 'stream_messages', messages: streamMessages() };
}
// interactive путь: исходная реализация без измененийОбоснование: Режим non_interactive (конвейер CLI) не поддерживает stream_messages, может вернуть только одно message; режим ACP (плагин IDE) может потреблять stream_messages и отображать прогресс в реальном времени, поэтому для него сохраняется streaming-путь.
Формат ACP-сообщения: encodeInsightProgressMessage(stage, progress, detail?) создаёт сообщение с прогресс-баром, которое может быть интерпретировано IDE; encodeInsightReadyMessage(outputPath) уведомляет IDE, что файл готов, IDE решает, как отобразить ссылку.
7.4 /docs
Текущее состояние: Action возвращает void, отображая сообщение через addItem и открывая браузер через open(docsUrl). Существует ветвь по переменной окружения SANDBOX (в песочнице только addItem, без открытия браузера).
Изменение:
- Изменить
supportedModesна['interactive', 'non_interactive', 'acp']. - Изменить возвращаемый тип action на
Promise<void | MessageActionReturn>. - Вставить ветвь non-interactive в начале action:
action: async (context) => {
const langPath = getCurrentLanguage()?.startsWith('zh') ? 'zh' : 'en';
const docsUrl = `https://qwenlm.github.io/qwen-code-docs/${langPath}`;
if (context.executionMode !== 'interactive') {
// Неинтерактивный/ACP: напрямую вернуть URL, не открывать браузер, не вызывать addItem
return {
type: 'message',
messageType: 'info',
content: `Qwen Code documentation: ${docsUrl}`,
};
}
// interactive путь: исходная проверка SANDBOX + addItem + open() без изменений
if (process.env['SANDBOX'] && ...) {
context.ui.addItem(...);
} else {
context.ui.addItem(...);
await open(docsUrl);
}
},7.5 /clear (altNames: reset, new)
Текущее состояние: Action выполняет следующие операции и возвращает void:
config.getHookSystem()?.fireSessionEndEvent()— запуск хука (побочный эффект)config.startNewSession()— запуск новой сессии (побочный эффект)uiTelemetryService.reset()— сброс счётчиков телеметрии (побочный эффект)skillTool.clearLoadedSkills()— очистка кэша скиллов (побочный эффект)context.ui.clear()— очистка UI терминала (побочный эффект UI; в non-interactive — no-op)geminiClient.resetChat()— сброс истории чата (побочный эффект)config.getHookSystem()?.fireSessionStartEvent()— запуск хука (побочный эффект)
Анализ семантики в non-interactive/ACP:
ui.clear()в non-interactive уже является no-op, дополнительной обработки не требуетсяgeminiClient.resetChat(): в ACP-сессии — осмысленный побочный эффект (очистка истории чата); должен быть сохранён. При однократном вызове в non-interactive каждый вызов — это новая сессия,resetChatсемантически избыточен, но безвреден.config.startNewSession(): в ACP имеет смысл (начало нового ID сессии); в non-interactive также избыточен, но безвреден.fireSessionEndEvent/fireSessionStartEvent: в ACP имеют смысл (запуск хуков).
Решение: Для non-interactive/ACP пути сохранить все осмысленные побочные эффекты (resetChat, startNewSession, hook events), пропустить только ui.clear() (уже no-op) и вернуть сообщение-метку границы контекста.
Изменение:
- Изменить
supportedModesна['interactive', 'non_interactive', 'acp']. - Изменить возвращаемый тип action на
Promise<void | MessageActionReturn>. - Внутри action, после (или вместо) вызова
context.ui.clear(), разветвление по режиму:
action: async (context, _args) => {
const { config } = context.services;
if (config) {
config.getHookSystem()?.fireSessionEndEvent(SessionEndReason.Clear).catch(...);
const newSessionId = config.startNewSession();
uiTelemetryService.reset();
const skillTool = config.getToolRegistry()?.getAllTools().find(...);
if (skillTool instanceof SkillTool) skillTool.clearLoadedSkills();
if (newSessionId && context.session.startNewSession) {
context.session.startNewSession(newSessionId);
}
// ui.clear() в неинтерактивном режиме уже no-op, но мы всё равно вызываем (условие не нужно)
context.ui.clear();
const geminiClient = config.getGeminiClient();
if (geminiClient) {
await geminiClient.resetChat();
}
config.getHookSystem()?.fireSessionStartEvent(...).catch(...);
} else {
context.ui.clear();
}
// Определить возвращаемое значение в зависимости от режима
if (context.executionMode !== 'interactive') {
return {
type: 'message',
messageType: 'info',
content: 'Context cleared. Previous messages are no longer in context.',
};
}
// interactive путь: void (ничего не возвращаем, React UI обновляется через ui.clear())
},Семантика ACP: IDE, получив метку границы контекста, может отобразить её как разделитель сессии (например, подсказку «Новый сеанс») и очистить локальный кэш истории чата.
8. Изменения в handleCommandResult
Вывод: Изменения не требуются.
После изменений в Фазе 2 для всех команд в non-interactive/ACP пути возвращаемые типы будут message или submit_prompt, которые уже корректно обрабатываются в switch операторе handleCommandResult.
9. Изменения в createNonInteractiveUI()
Вывод: Изменения не требуются.
Текущей no-op реализации достаточно. addItem, clear, setPendingItem и т.д. (no-op) не будут вызываться в non-interactive путях команд категории B (из-за досрочного return); на интерактивные пути это не влияет.
10. Фаза 2.2: Реализация вызова модели для prompt command
В Фазе 1 уже реализован CommandService.getModelInvocableCommands(); BundledSkillLoader, FileCommandLoader (пользовательские/проектные команды), McpPromptLoader уже устанавливают modelInvocable: true.
Задача Фазы 2.2 — перевести SkillTool с использования только SkillManager.listSkills() на одновременное использование CommandService.getModelInvocableCommands(), унифицировав точку входа для вызываемых моделью команд.
Файл изменений: packages/core/src/tools/SkillTool.ts (или соответствующий путь)
Конкретные изменения:
SkillToolпри инициализации получаетCommandService(или результат егоgetModelInvocableCommands()) через dependency injection- При построении tool description объединяет результаты
listSkills()иgetModelInvocableCommands() - Гарантирует, что built-in команды (
modelInvocable: false) не попадают в tool description
Примечание: Конкретная реализация
SkillToolзависит от внутренней архитектурыpackages/core; в данном документе описываются только изменения интерфейса; детали реализации следует определять с учётом существующей структуры core-пакета.
11. Фаза 2.3: Обнаружение slash command в середине ввода (базовая версия)
В компоненте InputPrompt обнаружить токен slash рядом с курсором (не обязательно в начале строки) и вызвать меню автодополнения.
Правило обнаружения:
- Если перед курсором есть токен, начинающийся с
/и не содержащий пробелов, запускать автодополнение команды - Кандидаты для дополнения берутся из списка видимых команд
getCommandsForMode('interactive') - Меню дополнения отображает имя команды + описание (без argumentHint и т.д.; это будет добавлено в Фазе 3)
Данная функциональность относится к изменениям UI-уровня и является независимой подзадачей Фазы 2.3, не влияющей на реализацию Фаз 2.1/2.2.
12. Общий перечень файловых изменений
12.1 Изменения в файлах команд (Фаза 2.1)
| Файл | Тип изменения | Подробности |
|---|---|---|
exportCommand.ts | Категория A | Родительская команда + 4 подкоманды: supportedModes → все режимы |
planCommand.ts | Только инт. | Дизайн-решение: оставлено supportedModes: ['interactive'], без изменений |
statuslineCommand.ts | Только инт. | Дизайн-решение: оставлено supportedModes: ['interactive'], без изменений |
languageCommand.ts | Категория A+ | Родительская команда + подкоманды ui/output + динамические подкоманды языка: supportedModes → все режимы |
copyCommand.ts | Только инт. | Дизайн-решение: оставлено supportedModes: ['interactive'], без изменений |
restoreCommand.ts | Только инт. | Дизайн-решение: оставлено supportedModes: ['interactive'], без изменений |
modelCommand.ts | Категория A’ | supportedModes → все режимы + добавлена неинтерактивная ветвь для пути без параметров / без fast model |
approvalModeCommand.ts | Категория A’ | supportedModes → все режимы + добавлена неинтерактивная ветвь для пути без параметров |
aboutCommand.ts | Категория B | supportedModes → все режимы + неинтерактивный путь возвращает message (версия/модель/сводка окружения) |
statsCommand.ts | Категория B | supportedModes → все режимы + неинтерактивный путь возвращает message (текст статистики); подкоманды обработаны синхронно |
insightCommand.ts | Категория B | supportedModes → все режимы + non_interactive путь — синхронная генерация, возвращает message (путь к файлу); acp путь — возвращает stream_messages с прогрессом |
docsCommand.ts | Категория B | supportedModes → все режимы + неинтерактивный путь возвращает message (URL документации), без открытия браузера |
clearCommand.ts | Категория B | supportedModes → все режимы + в конце action в зависимости от режима возвращает message или void |
12.2 Изменения в других файлах
| Файл | Описание изменений |
|---|---|
packages/core/src/tools/SkillTool.ts | Phase 2.2: интеграция getModelInvocableCommands() (детали будут уточнены) |
packages/cli/src/ui/InputPrompt.tsx (или эквивалент) | Phase 2.3: логика обнаружения слэша в середине ввода |
12.3 Неизменяемые файлы
packages/cli/src/nonInteractiveCliCommands.ts(handleCommandResult,handleSlashCommandне требуют изменений)packages/cli/src/ui/noninteractive/nonInteractiveUi.ts(заглушка UI не требует изменений)packages/cli/src/services/commandUtils.ts(filterCommandsForMode,getEffectiveSupportedModesне требуют изменений)packages/cli/src/services/CommandService.ts(getCommandsForMode,getModelInvocableCommandsуже реализованы в Phase 1)
13. Стратегия тестирования
13.1 Модульные тесты команд
Для каждой изменённой команды создайте или обновите файлы тестов (*.test.ts) в той же директории, покрывающие следующие кейсы:
Команды класса A/A+ (export, language):
supportedModesкорректно содержитnon_interactiveиacp- В режиме
executionMode: 'non_interactive'action возвращаетMessageActionReturnилиSubmitPromptActionReturn, не вызываяui.addItemилиui.clear - Поведение в интерактивном режиме полностью соответствует состоянию до рефакторинга (snapshot-тестирование)
Команды только для интерактивного режима (plan, statusline, copy, restore):
supportedModesравно['interactive']— это архитектурное решение- Проверить, что при выполнении в non-interactive режиме корректно возвращается
unsupported
Команды класса A’ (model, approval-mode):
- Без аргументов +
executionMode: 'non_interactive'→ возвращает сообщение о текущем состоянии (message), не возвращаетdialog - С аргументами +
executionMode: 'non_interactive'→ существующая логикаmessageвыполняется корректно - Интерактивный режим: без аргументов →
dialog, с аргументами →message(без изменений)
Команды класса B (about, stats, insight, docs, clear):
- В режиме
executionMode: 'non_interactive'action возвращаетMessageActionReturn, не вызывая никаких методовui.* - Возвращаемая строка
contentсодержит ожидаемые ключевые поля (номер версии, имя модели, URL и т.д.) - Интерактивный режим: вызывается
ui.addItem, action возвращаетvoid(без изменений)
Особый случай clear:
- В режиме
executionMode: 'non_interactive'всё ещё вызываетсяgeminiClient.resetChat()(побочный эффект сохраняется) - Возвращается граничное сообщение (
message) с содержимым'Context cleared. Previous messages are no longer in context.'
13.2 Интеграционные тесты (handleSlashCommand)
В файле nonInteractiveCli.test.ts или новом файле интеграционных тестов:
handleSlashCommand('/about', ...)в non-interactive режиме возвращает{ type: 'message', content: содержит номер версии }handleSlashCommand('/stats', ...)в non-interactive режиме возвращает{ type: 'message', content: содержит 'Session duration' }handleSlashCommand('/docs', ...)в non-interactive режиме возвращает{ type: 'message', content: содержит 'qwenlm.github.io' }handleSlashCommand('/clear', ...)в non-interactive режиме возвращает{ type: 'message', content: 'Context cleared.' }handleSlashCommand('/plan', ...)в non-interactive режиме возвращаетunsupported(команда только для интерактивного режима)- Существующие команды non-interactive (
btw,bugи т.д.) не деградируют
13.3 Тесты commandUtils
В файле commandUtils.test.ts добавляются (или существующие тесты продолжают покрывать):
- Все расширенные команды (
export,languageи т.д.) проходят фильтрациюfilterCommandsForMode(commands, 'non_interactive')иfilterCommandsForMode(commands, 'acp') - Команды только для интерактивного режима (
plan,statusline,copy,restore) корректно отфильтровываются при вызовеfilterCommandsForMode(commands, 'non_interactive')
14. Анализ влияния на поведение
| Сценарий | Поведение до Phase 2 | Поведение после Phase 2 | Характер |
|---|---|---|---|
non-interactive: /export md | ❌ unsupported (отфильтрована) | ✅ возвращает message с путём файла | Расширение возможностей |
non-interactive: /plan <task> | ❌ unsupported | ❌ unsupported (решение: только интерактив) | Без изменений |
non-interactive: /statusline | ❌ unsupported | ❌ unsupported (решение: только интерактив) | Без изменений |
non-interactive: /language ui zh-CN | ❌ unsupported | ✅ устанавливает язык, возвращает подтверждение | Расширение возможностей |
non-interactive: /copy | ❌ unsupported | ❌ unsupported (решение: только интерактив) | Без изменений |
non-interactive: /restore (без аргументов) | ❌ unsupported | ❌ unsupported (решение: только интерактив) | Без изменений |
non-interactive: /restore <id> | ❌ unsupported | ❌ unsupported (решение: только интерактив) | Без изменений |
non-interactive: /model | ❌ unsupported (dialog) | ✅ возвращает имя текущей модели | Расширение возможностей |
non-interactive: /model <id> | ❌ unsupported | 🔄 Phase 2 опционально: реализовать логику переключения | Расширение возможностей (опционально) |
non-interactive: /approval-mode | ❌ unsupported (dialog) | ✅ возвращает текущий режим одобрения | Расширение возможностей |
non-interactive: /approval-mode yolo | ❌ unsupported | ✅ устанавливает режим, возвращает подтверждение | Расширение возможностей |
non-interactive: /about | ❌ возвращает “Command executed successfully.” (addItem no-op) | ✅ возвращает сводку: версия/модель/окружение | Исправление бага + расширение |
non-interactive: /stats | ❌ возвращает “Command executed successfully.” | ✅ возвращает статистику сессии | Исправление бага + расширение |
non-interactive: /insight | ❌ возвращает “Command executed successfully.” (создаётся, но нет вывода) | ✅ создаёт и возвращает путь к файлу | Исправление бага + расширение |
non-interactive: /docs | ❌ возвращает “Command executed successfully.” | ✅ возвращает URL документации | Исправление бага + расширение |
non-interactive: /clear | ❌ возвращает “Command executed successfully.” | ✅ возвращает граничное сообщение контекста | Исправление бага + расширение |
| Интерактивный режим: любая из команд | ✅ существующее поведение | ✅ существующее поведение (нулевая деградация) | Без изменений |
15. Порядок реализации
Рекомендуется реализовывать в указанном порядке; каждая группа может быть закоммичена и проверена независимо:
Пакет 1 (~30 мин): Класс A — только изменение supportedModes
Изменить exportCommand.ts (и его подкоманды), убедиться, что тесты проходят.
Пакет 2 (~45 мин): Класс A+ — небольшое количество ветвлений
Изменить languageCommand.ts, добавить ветвь для неинтерактивного режима в путях с побочными эффектами, обновить соответствующие тесты. (copyCommand.ts и restoreCommand.ts после обсуждения остаются только для интерактивного режима.)
Пакет 3 (~45 мин): Класс A’ — пути dialog
Изменить modelCommand.ts, approvalModeCommand.ts, добавить ветвь для неинтерактивного режима в путях без аргументов, обновить соответствующие тесты.
Пакет 4 (~1.5 ч): Класс B — полные ветви
Изменить aboutCommand.ts, statsCommand.ts (включая подкоманды), docsCommand.ts.
Пакет 5 (~1 ч): Особые команды класса B — insightCommand.ts, clearCommand.ts
У этих команд много побочных эффектов, выделить в отдельный коммит, обновить соответствующие тесты и интеграционные тесты.
Пакет 6 (~2 ч): Phase 2.2 — интеграция вызова модели с prompt command
Изменить SkillTool, подключить getModelInvocableCommands(), обновить тесты SkillTool.
Пакет 7 (~2 ч): Phase 2.3 — обнаружение слэша в середине ввода
Изменить компонент InputPrompt, добавить логику триггера автодополнения и UI-тесты.
Пакет 8 (~30 мин): Полное тестирование + проверка типов
Запустить npm run typecheck, cd packages/cli && npx vitest run, исправить оставшиеся проблемы.
16. Чек-лист приёмки
Phase 2.1 — Расширение команд
- Класс A:
/export(и подкоманды),/plan,/statuslineкорректно выполняются в режимах non-interactive и acp и возвращают осмысленный вывод - Класс A+:
/language(и подкоманды) корректно выполняются в non-interactive режиме, сохраняют настройки - Класс A+:
/copyв non-interactive/acp режиме возвращает последний выведенный AI текст (не взаимодействует с буфером обмена) - Класс A+:
/restoreбез аргументов в non-interactive режиме возвращает список контрольных точек; с аргументом восстанавливает состояние и возвращает подтверждающее сообщение (не возвращаетtype: 'tool') - Класс A’:
/modelбез аргументов в non-interactive/acp режиме возвращает имя текущей модели (не открывает dialog);/model --fast <id>корректно устанавливает - Класс A’:
/approval-modeбез аргументов в non-interactive/acp режиме возвращает текущий режим (не открывает dialog); с аргументом корректно устанавливает - Класс B:
/aboutв non-interactive/acp режиме возвращает текстовую сводку, содержащую номер версии и имя модели - Класс B:
/stats(включая подкоманды) в non-interactive/acp режиме возвращает текстовую статистику - Класс B:
/insightв non-interactive/acp режиме создаёт файл insight и возвращает путь к файлу (не открывает браузер) - Класс B:
/docsв non-interactive/acp режиме возвращает URL документации (не открывает браузер) - Класс B:
/clearв non-interactive/acp режиме возвращает граничное сообщение контекста,geminiClient.resetChat()выполняется корректно - Все 13 команд в интерактивном режиме ведут себя полностью так же, как до рефакторинга (нулевая деградация)
- Компиляция TypeScript без ошибок (
npm run typecheck) -
npm run lintбез новых ошибок - Все существующие тесты проходят (
cd packages/cli && npx vitest run)
Phase 2.2 — Вызов модели
- Модель может вызывать через
SkillToolbundled skill, file command (пользовательские/проектные), MCP prompt - Модель не может вызывать built-in commands
- Описание инструмента
SkillToolсодержит имена и описания всех команд с флагомmodelInvocable: true
Phase 2.3 — Слэш в середине ввода
- При вводе
/в середине текста в поле ввода появляется меню автодополнения команд (не только в начале строки) - В меню автодополнения отображаются имя команды и описание
- После выбора команды она корректно вставляется в поле ввода