Skip to content

Commit d217003

Browse files
committed
feat: add deep research command with multi-step web research workflow and override role functionality
1 parent dbb2809 commit d217003

14 files changed

Lines changed: 461 additions & 5 deletions

File tree

source/hooks/conversation/useCommandHandler.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,24 @@ export function useCommandHandler(options: CommandHandlerOptions) {
11611161
options.setMessages([commandMessage]);
11621162
// Auto-send the review prompt using advanced model (not basic model), hide the prompt from UI
11631163
options.processMessage(result.prompt, undefined, false, true);
1164+
} else if (
1165+
result.success &&
1166+
result.action === 'deepResearch' &&
1167+
result.prompt
1168+
) {
1169+
// Deep Research command: run as a normal advanced-model task while
1170+
// hiding the (very long) embedded prompt from the chat history.
1171+
// Show the original (truncated) user request under the command tree
1172+
// node — `result.message` is set by deepresearch.ts to the truncated
1173+
// user prompt, which formatCommandResultLines() renders as `└─ ...`.
1174+
const commandMessage: Message = {
1175+
role: 'command',
1176+
content: result.message || '',
1177+
commandName: commandName,
1178+
};
1179+
options.setMessages(prev => [...prev, commandMessage]);
1180+
// Use advanced model (basicModel=false) and hide the prompt from UI
1181+
options.processMessage(result.prompt, undefined, false, true);
11641182
} else if (result.success && result.action === 'exportChat') {
11651183
// Handle export chat command
11661184
// Show loading message first

source/hooks/ui/useCommandPanel.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const COMMAND_ARGS_HINTS: Record<string, string> = {
4040
'role-subagent': '[-l|--list | -d|--delete]',
4141
'subagent-depth': '[<number>|status]',
4242
btw: '<question>',
43+
deepresearch: '<prompt>',
4344
connect: '[apiUrl]',
4445
};
4546

@@ -287,6 +288,12 @@ export function useCommandPanel(buffer: TextBuffer, isProcessing = false) {
287288
allowDuringProcessing: true,
288289
mainFlowOnly: true,
289290
},
291+
{
292+
name: 'deepresearch',
293+
description:
294+
t.commandPanel.commands.deepresearch ||
295+
'Run an autonomous web research workflow and save a cited markdown report to .snow/deepresearch/',
296+
},
290297
],
291298
[t],
292299
);

source/i18n/lang/en.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,8 @@ export const en: TranslationKeys = {
655655
newPrompt: 'Generate a refined prompt from your requirement using AI',
656656
pixel: 'Open the terminal pixel editor',
657657
btw: 'Ask a side-question while AI is working (temporary, no context saved)',
658+
deepresearch:
659+
'Run an autonomous multi-step web research workflow and save a cited markdown report to .snow/deepresearch/',
658660
quit: 'Exit the application',
659661
},
660662
copyLastFeedback: {
@@ -696,6 +698,11 @@ export const en: TranslationKeys = {
696698
'Conversation forked into branch {name}. To return to the original session:\n/resume {originalId}',
697699
failed: 'Failed to fork session',
698700
},
701+
// Deep Research command messages
702+
deepResearch: {
703+
usage:
704+
'Usage: /deepresearch <prompt>\nExample: /deepresearch Compare the architectures of OpenAI Deep Research and Gemini Deep Research',
705+
},
699706
},
700707
},
701708
fileList: {
@@ -1327,10 +1334,14 @@ export const en: TranslationKeys = {
13271334
deleteSuccess: 'Role deleted successfully',
13281335
loading: 'Processing...',
13291336
hints:
1330-
'Tab: Switch scope | Enter: Activate | N: New | D: Delete | ESC: Close',
1337+
'Tab: Switch scope | Enter: Activate | N: New | D: Delete | R: Override prompt | ESC: Close',
13311338
cannotDeleteActive: 'Cannot delete active role',
13321339
confirmDelete: 'Confirm delete this role?',
13331340
confirmDeleteHint: 'Press Y to confirm, N to cancel',
1341+
overrideTag: 'Override',
1342+
overrideEnabled: 'Enabled: this role overrides the system prompt',
1343+
overrideDisabled: 'Disabled: default system prompt restored',
1344+
cannotOverrideInactive: 'Only the active role can be marked as override',
13341345
},
13351346

13361347
roleSubagentCreation: {

source/i18n/lang/zh-TW.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,8 @@ export const zhTW: TranslationKeys = {
614614
newPrompt: '根據需求使用 AI 生成精煉的提示詞',
615615
pixel: '開啟終端像素編輯器',
616616
btw: '在 AI 運行時快速提問(臨時對話,不儲存上下文)',
617+
deepresearch:
618+
'執行自主多步聯網深度研究,並將帶引用的 Markdown 報告儲存到 .snow/deepresearch/',
617619
quit: '退出應用程式',
618620
},
619621
copyLastFeedback: {
@@ -654,6 +656,11 @@ export const zhTW: TranslationKeys = {
654656
'對話已分叉為分支 {name}。返回原會話請執行:\n/resume {originalId}',
655657
failed: '會話分叉失敗',
656658
},
659+
// Deep Research 命令訊息
660+
deepResearch: {
661+
usage:
662+
'用法: /deepresearch <提示詞>\n範例: /deepresearch 對比 OpenAI Deep Research 與 Gemini Deep Research 的架構差異',
663+
},
657664
},
658665
},
659666
fileList: {
@@ -1251,10 +1258,15 @@ export const zhTW: TranslationKeys = {
12511258
createSuccess: '角色創建成功',
12521259
deleteSuccess: '角色刪除成功',
12531260
loading: '處理中...',
1254-
hints: 'Tab: 切換作用域 | Enter: 啟用 | N: 新建 | D: 刪除 | ESC: 關閉',
1261+
hints:
1262+
'Tab: 切換作用域 | Enter: 啟用 | N: 新建 | D: 刪除 | R: 覆蓋系統提示詞 | ESC: 關閉',
12551263
cannotDeleteActive: '無法刪除啟用的角色',
12561264
confirmDelete: '確認刪除該角色?',
12571265
confirmDeleteHint: '按 Y 確認,按 N 取消',
1266+
overrideTag: '覆蓋',
1267+
overrideEnabled: '已啟用:使用該角色覆蓋系統提示詞',
1268+
overrideDisabled: '已關閉:恢復使用預設系統提示詞',
1269+
cannotOverrideInactive: '只有啟用的角色才能標記為覆蓋',
12581270
},
12591271

12601272
roleSubagentCreation: {

source/i18n/lang/zh.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,8 @@ export const zh: TranslationKeys = {
613613
newPrompt: '根据需求使用 AI 生成精炼的提示词',
614614
pixel: '打开终端像素编辑器',
615615
btw: '在 AI 运行时快速提问(临时对话,不保存上下文)',
616+
deepresearch:
617+
'执行自主多步联网深度研究,并将带引用的 Markdown 报告保存到 .snow/deepresearch/',
616618
quit: '退出应用程序',
617619
},
618620
copyLastFeedback: {
@@ -653,6 +655,11 @@ export const zh: TranslationKeys = {
653655
'对话已分叉为分支 {name}。返回原会话请执行:\n/resume {originalId}',
654656
failed: '会话分叉失败',
655657
},
658+
// Deep Research 命令消息
659+
deepResearch: {
660+
usage:
661+
'用法: /deepresearch <提示词>\n示例: /deepresearch 对比 OpenAI Deep Research 与 Gemini Deep Research 的架构差异',
662+
},
656663
},
657664
},
658665
fileList: {
@@ -1251,10 +1258,15 @@ export const zh: TranslationKeys = {
12511258
createSuccess: '角色创建成功',
12521259
deleteSuccess: '角色删除成功',
12531260
loading: '处理中...',
1254-
hints: 'Tab: 切换作用域 | Enter: 激活 | N: 新建 | D: 删除 | ESC: 关闭',
1261+
hints:
1262+
'Tab: 切换作用域 | Enter: 激活 | N: 新建 | D: 删除 | R: 覆盖系统提示词 | ESC: 关闭',
12551263
cannotDeleteActive: '无法删除激活的角色',
12561264
confirmDelete: '确认删除该角色?',
12571265
confirmDeleteHint: '按 Y 确认,按 N 取消',
1266+
overrideTag: '覆盖',
1267+
overrideEnabled: '已启用:使用该角色覆盖系统提示词',
1268+
overrideDisabled: '已关闭:恢复使用默认系统提示词',
1269+
cannotOverrideInactive: '只有激活的角色才能标记为覆盖',
12581270
},
12591271

12601272
roleSubagentCreation: {

source/i18n/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,7 @@ export type TranslationKeys = {
604604
newPrompt: string;
605605
pixel: string;
606606
btw: string;
607+
deepresearch: string;
607608
quit: string;
608609
};
609610
copyLastFeedback: {
@@ -641,6 +642,10 @@ export type TranslationKeys = {
641642
success: string;
642643
failed: string;
643644
};
645+
// Deep Research command messages
646+
deepResearch: {
647+
usage: string;
648+
};
644649
};
645650
};
646651
// File search list (`@` panel)
@@ -1244,6 +1249,10 @@ export type TranslationKeys = {
12441249
cannotDeleteActive: string;
12451250
confirmDelete: string;
12461251
confirmDeleteHint: string;
1252+
overrideTag: string;
1253+
overrideEnabled: string;
1254+
overrideDisabled: string;
1255+
cannotOverrideInactive: string;
12471256
};
12481257
roleSubagentCreation: {
12491258
title: string;

source/prompt/shared/promptHelpers.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,83 @@ ${systemEnv}
222222
Current Date: ${timeInfo.date}`;
223223
}
224224

225+
/**
226+
* Read raw content of the active ROLE file IF it is marked as "override system prompt".
227+
* Priority: project > global. Returns null if no active role is marked as override
228+
* or if the role file is missing/empty.
229+
*/
230+
export function getOverrideRoleContent(): string | null {
231+
const tryReadRole = (rolePath: string): string | null => {
232+
try {
233+
if (!fs.existsSync(rolePath)) return null;
234+
const content = fs.readFileSync(rolePath, 'utf-8').trim();
235+
return content || null;
236+
} catch {
237+
return null;
238+
}
239+
};
240+
241+
const resolveActiveOverride = (
242+
location: 'project' | 'global',
243+
): {path: string; isOverride: boolean} | null => {
244+
try {
245+
const baseDir =
246+
location === 'project'
247+
? process.cwd()
248+
: path.join(os.homedir(), '.snow');
249+
const configPath =
250+
location === 'project'
251+
? path.join(baseDir, '.snow', 'role.json')
252+
: path.join(baseDir, 'role.json');
253+
254+
let activeRoleId: string | undefined;
255+
let overrideRoleIds: string[] = [];
256+
if (fs.existsSync(configPath)) {
257+
try {
258+
const raw = fs.readFileSync(configPath, 'utf-8');
259+
const parsed = JSON.parse(raw) as {
260+
activeRoleId?: string;
261+
overrideRoleIds?: string[];
262+
};
263+
activeRoleId = parsed.activeRoleId;
264+
overrideRoleIds = parsed.overrideRoleIds || [];
265+
} catch {
266+
// ignore
267+
}
268+
}
269+
270+
const resolvedActiveId =
271+
!activeRoleId || activeRoleId === 'active' ? 'active' : activeRoleId;
272+
const isOverride = overrideRoleIds.includes(resolvedActiveId);
273+
const filePath =
274+
resolvedActiveId === 'active'
275+
? path.join(baseDir, 'ROLE.md')
276+
: path.join(baseDir, `ROLE-${resolvedActiveId}.md`);
277+
return {path: filePath, isOverride};
278+
} catch {
279+
return null;
280+
}
281+
};
282+
283+
try {
284+
const projectInfo = resolveActiveOverride('project');
285+
if (projectInfo && projectInfo.isOverride) {
286+
const content = tryReadRole(projectInfo.path);
287+
if (content) return content;
288+
}
289+
290+
const globalInfo = resolveActiveOverride('global');
291+
if (globalInfo && globalInfo.isOverride) {
292+
const content = tryReadRole(globalInfo.path);
293+
if (content) return content;
294+
}
295+
} catch (error) {
296+
console.error('Failed to read override ROLE configuration:', error);
297+
}
298+
299+
return null;
300+
}
301+
225302
/**
226303
* Get the tool discovery section based on whether tool search is disabled
227304
*/

source/prompt/systemPrompt.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
appendSystemContext,
1111
detectWindowsPowerShell,
1212
getToolDiscoverySection as getToolDiscoverySectionHelper,
13+
getOverrideRoleContent,
1314
} from './shared/promptHelpers.js';
1415
import os from 'os';
1516

@@ -406,6 +407,19 @@ Tools are loaded on-demand to save context. At the start of each conversation, o
406407

407408
// Export SYSTEM_PROMPT as a getter function for real-time ROLE.md updates
408409
export function getSystemPrompt(toolSearchDisabled = false): string {
410+
// If the active role is marked as "override", its content REPLACES the
411+
// default system prompt entirely. Only system environment + date are appended.
412+
const overrideContent = getOverrideRoleContent();
413+
if (overrideContent) {
414+
const systemEnvOverride = getSystemEnvironmentInfoHelper(true);
415+
const timeInfoOverride = getCurrentTimeInfo();
416+
return appendSystemContext(
417+
overrideContent,
418+
systemEnvOverride,
419+
timeInfoOverride,
420+
);
421+
}
422+
409423
const basePrompt = getSystemPromptWithRoleHelper(
410424
SYSTEM_PROMPT_TEMPLATE,
411425
'You are Snow AI CLI, an intelligent command-line assistant.',

0 commit comments

Comments
 (0)