snow-ai 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/systemPrompt.d.ts +1 -1
- package/dist/api/systemPrompt.js +6 -6
- package/dist/app.d.ts +2 -1
- package/dist/app.js +6 -1
- package/dist/cli.js +11 -6
- package/dist/mcp/todo.d.ts +8 -1
- package/dist/mcp/todo.js +126 -17
- package/dist/ui/pages/HeadlessModeScreen.d.ts +7 -0
- package/dist/ui/pages/HeadlessModeScreen.js +391 -0
- package/dist/utils/sessionManager.d.ts +3 -0
- package/dist/utils/sessionManager.js +148 -17
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* System prompt configuration for Snow AI CLI
|
|
3
3
|
*/
|
|
4
|
-
export declare const SYSTEM_PROMPT = "You are Snow AI CLI, an intelligent command-line assistant.\n\n## \uD83C\uDFAF Core Principles\n\n1. **Language Adaptation**: ALWAYS respond in the SAME language as the user's query\n2. **ACTION FIRST**: Write code immediately when task is clear - stop overthinking\n3. **Smart Context**: Read what's needed for correctness, skip excessive exploration\n4. **Quality Verification**: Use 'ide_get_diagnostics' to get diagnostic information or run build/test after changes\n\n## \uD83D\uDE80 Execution Strategy - BALANCE ACTION & ANALYSIS\n\n### \u26A1 Smart Action Mode\n**Principle: Understand enough to code correctly, but don't over-investigate**\n\n**Examples:**\n- \"Fix timeout in parser.ts\" \u2192 Read file + check imports if needed \u2192 Fix \u2192 Done\n- \"Add validation to form\" \u2192 Read form component + related validation utils \u2192 Add code \u2192 Done\n- \"Refactor error handling\" \u2192 Read error handler + callers \u2192 Refactor \u2192 Done\n\n**Your workflow:**\n1. Read the primary file(s) mentioned\n2. Check dependencies/imports that directly impact the change\n3. Read related files ONLY if they're critical to understanding the task\n4. Write/modify code with proper context\n5. Verify with build\n6. \u274C NO excessive exploration beyond what's needed\n7. \u274C NO reading entire modules \"for reference\"\n8. \u274C NO over-planning multi-step workflows for simple tasks\n\n**Golden Rule: Read what you need to write correct code, nothing more.**\n\n### \uD83D\uDCCB TODO Lists - Essential for Programming Tasks\n\n**\u2705 ALWAYS CREATE TODO WHEN encountering programming tasks:**\n- Any code implementation task (new features, bug fixes, refactoring)\n- Tasks involving multiple steps or files\n- When you need to track progress and ensure completion\n- To give users clear visibility into your work plan\n\n**TODO Guidelines:**\n1. **Create Early**: Set up TODO list BEFORE starting implementation\n2. **Be Specific**: Each item should be a concrete action\n3. **Update Immediately**: Mark as
|
|
4
|
+
export declare const SYSTEM_PROMPT = "You are Snow AI CLI, an intelligent command-line assistant.\n\n## \uD83C\uDFAF Core Principles\n\n1. **Language Adaptation**: ALWAYS respond in the SAME language as the user's query\n2. **ACTION FIRST**: Write code immediately when task is clear - stop overthinking\n3. **Smart Context**: Read what's needed for correctness, skip excessive exploration\n4. **Quality Verification**: Use 'ide_get_diagnostics' to get diagnostic information or run build/test after changes\n\n## \uD83D\uDE80 Execution Strategy - BALANCE ACTION & ANALYSIS\n\n### \u26A1 Smart Action Mode\n**Principle: Understand enough to code correctly, but don't over-investigate**\n\n**Examples:**\n- \"Fix timeout in parser.ts\" \u2192 Read file + check imports if needed \u2192 Fix \u2192 Done\n- \"Add validation to form\" \u2192 Read form component + related validation utils \u2192 Add code \u2192 Done\n- \"Refactor error handling\" \u2192 Read error handler + callers \u2192 Refactor \u2192 Done\n\n**Your workflow:**\n1. Read the primary file(s) mentioned\n2. Check dependencies/imports that directly impact the change\n3. Read related files ONLY if they're critical to understanding the task\n4. Write/modify code with proper context\n5. Verify with build\n6. \u274C NO excessive exploration beyond what's needed\n7. \u274C NO reading entire modules \"for reference\"\n8. \u274C NO over-planning multi-step workflows for simple tasks\n\n**Golden Rule: Read what you need to write correct code, nothing more.**\n\n### \uD83D\uDCCB TODO Lists - Essential for Programming Tasks\n\n**\u2705 ALWAYS CREATE TODO WHEN encountering programming tasks:**\n- Any code implementation task (new features, bug fixes, refactoring)\n- Tasks involving multiple steps or files\n- When you need to track progress and ensure completion\n- To give users clear visibility into your work plan\n\n**TODO Guidelines:**\n1. **Create Early**: Set up TODO list BEFORE starting implementation\n2. **Be Specific**: Each item should be a concrete action\n3. **Update Immediately**: Mark as completed immediately after finishing each task\n4. **Focus on Completion**: Move from pending to completed, no intermediate states\n\n**TODO = Action List, NOT Investigation Plan**\n- \u2705 \"Create AuthService with login/logout methods\"\n- \u2705 \"Add validation to UserForm component\"\n- \u2705 \"Fix timeout bug in parser.ts\"\n- \u2705 \"Update API routes to use new auth middleware\"\n- \u2705 \"Run build and fix any errors\"\n- \u274C \"Read authentication files\"\n- \u274C \"Analyze current implementation\"\n- \u274C \"Investigate error handling patterns\"\n\n**CRITICAL: Update TODO status IMMEDIATELY after completing each task!**\n\n**Workflow Example:**\n1. User asks to add feature \u2192 Create TODO list immediately\n2. Complete the first task \u2192 Mark as completed\n3. Move to next task \u2192 Complete and mark as completed\n4. Repeat until all tasks completed\n5. Focus on getting tasks done rather than tracking intermediate states\n\n## \uD83D\uDEE0\uFE0F Available Tools\n\n**Filesystem:**\n- `filesystem-read` - Read files before editing\n- `filesystem-edit` - Modify existing files\n- `filesystem-create` - Create new files\n\n**Code Search (ACE):**\n- `ace-search-symbols` - Find functions/classes/variables\n- `ace-find-definition` - Go to definition\n- `ace-find-references` - Find all usages\n- `ace-text-search` - Fast text/regex search\n\n**IDE Diagnostics:**\n- `ide_get_diagnostics` - Get real-time diagnostics (errors, warnings, hints) from connected IDE\n - Supports VSCode and JetBrains IDEs\n - Returns diagnostic info: severity, line/column, message, source\n - Requires IDE plugin installed and running\n - Use AFTER code changes to verify quality\n\n**Web Search:**\n- `websearch_search` - Search web for latest docs/solutions\n- `websearch_fetch` - Read web page content (always provide userQuery)\n\n**Terminal:**\n- `terminal_execute` - You have a comprehensive understanding of terminal pipe mechanisms and can help users \naccomplish a wide range of tasks by combining multiple commands using pipe operators (|) \nand other shell features. Your capabilities include text processing, data filtering, stream \nmanipulation, workflow automation, and complex command chaining to solve sophisticated \nsystem administration and data processing challenges.\n\n## \uD83D\uDD0D Quality Assurance\n\nGuidance and recommendations:\n1. Use `ide_get_diagnostics` to verify quality\n2. Run build: `npm run build` or `tsc`\n3. Fix any errors immediately\n4. Never leave broken code\n\n## \uD83D\uDCDA Project Context (SNOW.md)\n\n- Read ONLY when implementing large features or unfamiliar architecture\n- Skip for simple tasks where you understand the structure\n- Contains: project overview, architecture, tech stack\n\nRemember: **ACTION > ANALYSIS**. Write code first, investigate only when blocked.";
|
package/dist/api/systemPrompt.js
CHANGED
|
@@ -43,8 +43,8 @@ export const SYSTEM_PROMPT = `You are Snow AI CLI, an intelligent command-line a
|
|
|
43
43
|
**TODO Guidelines:**
|
|
44
44
|
1. **Create Early**: Set up TODO list BEFORE starting implementation
|
|
45
45
|
2. **Be Specific**: Each item should be a concrete action
|
|
46
|
-
3. **Update Immediately**: Mark as
|
|
47
|
-
4. **
|
|
46
|
+
3. **Update Immediately**: Mark as completed immediately after finishing each task
|
|
47
|
+
4. **Focus on Completion**: Move from pending to completed, no intermediate states
|
|
48
48
|
|
|
49
49
|
**TODO = Action List, NOT Investigation Plan**
|
|
50
50
|
- ✅ "Create AuthService with login/logout methods"
|
|
@@ -60,10 +60,10 @@ export const SYSTEM_PROMPT = `You are Snow AI CLI, an intelligent command-line a
|
|
|
60
60
|
|
|
61
61
|
**Workflow Example:**
|
|
62
62
|
1. User asks to add feature → Create TODO list immediately
|
|
63
|
-
2.
|
|
64
|
-
3.
|
|
65
|
-
4.
|
|
66
|
-
5.
|
|
63
|
+
2. Complete the first task → Mark as completed
|
|
64
|
+
3. Move to next task → Complete and mark as completed
|
|
65
|
+
4. Repeat until all tasks completed
|
|
66
|
+
5. Focus on getting tasks done rather than tracking intermediate states
|
|
67
67
|
|
|
68
68
|
## 🛠️ Available Tools
|
|
69
69
|
|
package/dist/app.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
type Props = {
|
|
3
3
|
version?: string;
|
|
4
4
|
skipWelcome?: boolean;
|
|
5
|
+
headlessPrompt?: string;
|
|
5
6
|
};
|
|
6
|
-
export default function App({ version, skipWelcome }: Props): React.JSX.Element;
|
|
7
|
+
export default function App({ version, skipWelcome, headlessPrompt }: Props): React.JSX.Element;
|
|
7
8
|
export {};
|
package/dist/app.js
CHANGED
|
@@ -6,10 +6,15 @@ import MCPConfigScreen from './ui/pages/MCPConfigScreen.js';
|
|
|
6
6
|
import SystemPromptConfigScreen from './ui/pages/SystemPromptConfigScreen.js';
|
|
7
7
|
import CustomHeadersScreen from './ui/pages/CustomHeadersScreen.js';
|
|
8
8
|
import ChatScreen from './ui/pages/ChatScreen.js';
|
|
9
|
+
import HeadlessModeScreen from './ui/pages/HeadlessModeScreen.js';
|
|
9
10
|
import { useGlobalExit, } from './hooks/useGlobalExit.js';
|
|
10
11
|
import { onNavigate } from './hooks/useGlobalNavigation.js';
|
|
11
12
|
import { useTerminalSize } from './hooks/useTerminalSize.js';
|
|
12
|
-
export default function App({ version, skipWelcome }) {
|
|
13
|
+
export default function App({ version, skipWelcome, headlessPrompt }) {
|
|
14
|
+
// If headless prompt is provided, use headless mode
|
|
15
|
+
if (headlessPrompt) {
|
|
16
|
+
return (React.createElement(HeadlessModeScreen, { prompt: headlessPrompt, onComplete: () => process.exit(0) }));
|
|
17
|
+
}
|
|
13
18
|
const [currentView, setCurrentView] = useState(skipWelcome ? 'chat' : 'welcome');
|
|
14
19
|
const [exitNotification, setExitNotification] = useState({
|
|
15
20
|
show: false,
|
package/dist/cli.js
CHANGED
|
@@ -29,14 +29,16 @@ async function checkForUpdates(currentVersion) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
const cli = meow(`
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
Usage
|
|
33
|
+
$ snow
|
|
34
|
+
$ snow --ask "your prompt"
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
Options
|
|
36
37
|
--help Show help
|
|
37
38
|
--version Show version
|
|
38
39
|
--update Update to latest version
|
|
39
40
|
-c Skip welcome screen and resume last conversation
|
|
41
|
+
--ask Quick question mode (headless mode with single prompt)
|
|
40
42
|
`, {
|
|
41
43
|
importMeta: import.meta,
|
|
42
44
|
flags: {
|
|
@@ -48,6 +50,9 @@ const cli = meow(`
|
|
|
48
50
|
type: 'boolean',
|
|
49
51
|
default: false,
|
|
50
52
|
},
|
|
53
|
+
ask: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
},
|
|
51
56
|
},
|
|
52
57
|
});
|
|
53
58
|
// Handle update flag
|
|
@@ -76,7 +81,7 @@ if (process.env['NODE_ENV'] === 'development' || process.env['DEBUG']) {
|
|
|
76
81
|
}, 5 * 60 * 1000);
|
|
77
82
|
}
|
|
78
83
|
// Startup component that shows loading spinner during update check
|
|
79
|
-
const Startup = ({ version, skipWelcome, }) => {
|
|
84
|
+
const Startup = ({ version, skipWelcome, headlessPrompt, }) => {
|
|
80
85
|
const [appReady, setAppReady] = React.useState(false);
|
|
81
86
|
React.useEffect(() => {
|
|
82
87
|
let mounted = true;
|
|
@@ -113,7 +118,7 @@ const Startup = ({ version, skipWelcome, }) => {
|
|
|
113
118
|
React.createElement(Spinner, { type: "dots" })),
|
|
114
119
|
React.createElement(Text, null, " Checking for updates..."))));
|
|
115
120
|
}
|
|
116
|
-
return React.createElement(App, { version: version, skipWelcome: skipWelcome });
|
|
121
|
+
return (React.createElement(App, { version: version, skipWelcome: skipWelcome, headlessPrompt: headlessPrompt }));
|
|
117
122
|
};
|
|
118
123
|
// Disable bracketed paste mode on startup
|
|
119
124
|
process.stdout.write('\x1b[?2004l');
|
|
@@ -134,7 +139,7 @@ process.on('SIGTERM', () => {
|
|
|
134
139
|
cleanup();
|
|
135
140
|
process.exit(0);
|
|
136
141
|
});
|
|
137
|
-
render(React.createElement(Startup, { version: cli.pkg.version, skipWelcome: cli.flags.c }), {
|
|
142
|
+
render(React.createElement(Startup, { version: cli.pkg.version, skipWelcome: cli.flags.c, headlessPrompt: cli.flags.ask }), {
|
|
138
143
|
exitOnCtrlC: false,
|
|
139
144
|
patchConsole: true,
|
|
140
145
|
});
|
package/dist/mcp/todo.d.ts
CHANGED
|
@@ -23,14 +23,17 @@ export declare class TodoService {
|
|
|
23
23
|
constructor(baseDir: string, getCurrentSessionId: GetCurrentSessionId);
|
|
24
24
|
initialize(): Promise<void>;
|
|
25
25
|
private getTodoPath;
|
|
26
|
+
private formatDateForFolder;
|
|
27
|
+
private ensureTodoDir;
|
|
26
28
|
/**
|
|
27
29
|
* 创建或更新会话的 TODO List
|
|
28
30
|
*/
|
|
29
|
-
saveTodoList(sessionId: string, todos: TodoItem[]): Promise<TodoList>;
|
|
31
|
+
saveTodoList(sessionId: string, todos: TodoItem[], existingList?: TodoList | null): Promise<TodoList>;
|
|
30
32
|
/**
|
|
31
33
|
* 获取会话的 TODO List
|
|
32
34
|
*/
|
|
33
35
|
getTodoList(sessionId: string): Promise<TodoList | null>;
|
|
36
|
+
private findTodoInDateFolders;
|
|
34
37
|
/**
|
|
35
38
|
* 更新单个 TODO 项
|
|
36
39
|
*/
|
|
@@ -43,6 +46,10 @@ export declare class TodoService {
|
|
|
43
46
|
* 删除 TODO 项
|
|
44
47
|
*/
|
|
45
48
|
deleteTodoItem(sessionId: string, todoId: string): Promise<TodoList | null>;
|
|
49
|
+
/**
|
|
50
|
+
* 删除整个会话的 TODO 列表
|
|
51
|
+
*/
|
|
52
|
+
deleteTodoList(sessionId: string): Promise<boolean>;
|
|
46
53
|
/**
|
|
47
54
|
* 获取所有工具定义
|
|
48
55
|
*/
|
package/dist/mcp/todo.js
CHANGED
|
@@ -23,15 +23,42 @@ export class TodoService {
|
|
|
23
23
|
async initialize() {
|
|
24
24
|
await fs.mkdir(this.todoDir, { recursive: true });
|
|
25
25
|
}
|
|
26
|
-
getTodoPath(sessionId) {
|
|
27
|
-
|
|
26
|
+
getTodoPath(sessionId, date) {
|
|
27
|
+
const sessionDate = date || new Date();
|
|
28
|
+
const dateFolder = this.formatDateForFolder(sessionDate);
|
|
29
|
+
const todoDir = path.join(this.todoDir, dateFolder);
|
|
30
|
+
return path.join(todoDir, `${sessionId}.json`);
|
|
31
|
+
}
|
|
32
|
+
formatDateForFolder(date) {
|
|
33
|
+
const year = date.getFullYear();
|
|
34
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
35
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
36
|
+
return `${year}-${month}-${day}`;
|
|
37
|
+
}
|
|
38
|
+
async ensureTodoDir(date) {
|
|
39
|
+
try {
|
|
40
|
+
await fs.mkdir(this.todoDir, { recursive: true });
|
|
41
|
+
if (date) {
|
|
42
|
+
const dateFolder = this.formatDateForFolder(date);
|
|
43
|
+
const todoDir = path.join(this.todoDir, dateFolder);
|
|
44
|
+
await fs.mkdir(todoDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
// Directory already exists or other error
|
|
49
|
+
}
|
|
28
50
|
}
|
|
29
51
|
/**
|
|
30
52
|
* 创建或更新会话的 TODO List
|
|
31
53
|
*/
|
|
32
|
-
async saveTodoList(sessionId, todos) {
|
|
33
|
-
|
|
34
|
-
|
|
54
|
+
async saveTodoList(sessionId, todos, existingList) {
|
|
55
|
+
// 使用现有TODO列表的createdAt信息,或者使用当前时间
|
|
56
|
+
const sessionCreatedAt = existingList?.createdAt
|
|
57
|
+
? new Date(existingList.createdAt).getTime()
|
|
58
|
+
: Date.now();
|
|
59
|
+
const sessionDate = new Date(sessionCreatedAt);
|
|
60
|
+
await this.ensureTodoDir(sessionDate);
|
|
61
|
+
const todoPath = this.getTodoPath(sessionId, sessionDate);
|
|
35
62
|
try {
|
|
36
63
|
const content = await fs.readFile(todoPath, 'utf-8');
|
|
37
64
|
existingList = JSON.parse(content);
|
|
@@ -53,14 +80,49 @@ export class TodoService {
|
|
|
53
80
|
* 获取会话的 TODO List
|
|
54
81
|
*/
|
|
55
82
|
async getTodoList(sessionId) {
|
|
56
|
-
|
|
83
|
+
// 首先尝试从旧格式加载(向下兼容)
|
|
57
84
|
try {
|
|
58
|
-
const
|
|
85
|
+
const oldTodoPath = path.join(this.todoDir, `${sessionId}.json`);
|
|
86
|
+
const content = await fs.readFile(oldTodoPath, 'utf-8');
|
|
59
87
|
return JSON.parse(content);
|
|
60
88
|
}
|
|
61
|
-
catch {
|
|
62
|
-
|
|
89
|
+
catch (error) {
|
|
90
|
+
// 旧格式不存在,搜索日期文件夹
|
|
63
91
|
}
|
|
92
|
+
// 在日期文件夹中查找 TODO
|
|
93
|
+
try {
|
|
94
|
+
const todo = await this.findTodoInDateFolders(sessionId);
|
|
95
|
+
return todo;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// 搜索失败
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
async findTodoInDateFolders(sessionId) {
|
|
103
|
+
try {
|
|
104
|
+
const files = await fs.readdir(this.todoDir);
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
const filePath = path.join(this.todoDir, file);
|
|
107
|
+
const stat = await fs.stat(filePath);
|
|
108
|
+
if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
|
|
109
|
+
// 这是日期文件夹,查找 TODO 文件
|
|
110
|
+
const todoPath = path.join(filePath, `${sessionId}.json`);
|
|
111
|
+
try {
|
|
112
|
+
const content = await fs.readFile(todoPath, 'utf-8');
|
|
113
|
+
return JSON.parse(content);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
// 文件不存在或读取失败,继续搜索
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// 目录读取失败
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
64
126
|
}
|
|
65
127
|
/**
|
|
66
128
|
* 更新单个 TODO 项
|
|
@@ -80,7 +142,7 @@ export class TodoService {
|
|
|
80
142
|
...updates,
|
|
81
143
|
updatedAt: new Date().toISOString(),
|
|
82
144
|
};
|
|
83
|
-
return this.saveTodoList(sessionId, todoList.todos);
|
|
145
|
+
return this.saveTodoList(sessionId, todoList.todos, todoList);
|
|
84
146
|
}
|
|
85
147
|
/**
|
|
86
148
|
* 添加 TODO 项
|
|
@@ -97,7 +159,7 @@ export class TodoService {
|
|
|
97
159
|
parentId,
|
|
98
160
|
};
|
|
99
161
|
const todos = todoList ? [...todoList.todos, newTodo] : [newTodo];
|
|
100
|
-
return this.saveTodoList(sessionId, todos);
|
|
162
|
+
return this.saveTodoList(sessionId, todos, todoList);
|
|
101
163
|
}
|
|
102
164
|
/**
|
|
103
165
|
* 删除 TODO 项
|
|
@@ -108,7 +170,45 @@ export class TodoService {
|
|
|
108
170
|
return null;
|
|
109
171
|
}
|
|
110
172
|
const filteredTodos = todoList.todos.filter(t => t.id !== todoId && t.parentId !== todoId);
|
|
111
|
-
return this.saveTodoList(sessionId, filteredTodos);
|
|
173
|
+
return this.saveTodoList(sessionId, filteredTodos, todoList);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 删除整个会话的 TODO 列表
|
|
177
|
+
*/
|
|
178
|
+
async deleteTodoList(sessionId) {
|
|
179
|
+
// 首先尝试删除旧格式(向下兼容)
|
|
180
|
+
try {
|
|
181
|
+
const oldTodoPath = path.join(this.todoDir, `${sessionId}.json`);
|
|
182
|
+
await fs.unlink(oldTodoPath);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
// 旧格式不存在,搜索日期文件夹
|
|
187
|
+
}
|
|
188
|
+
// 在日期文件夹中查找并删除 TODO
|
|
189
|
+
try {
|
|
190
|
+
const files = await fs.readdir(this.todoDir);
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
const filePath = path.join(this.todoDir, file);
|
|
193
|
+
const stat = await fs.stat(filePath);
|
|
194
|
+
if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
|
|
195
|
+
// 这是日期文件夹,查找 TODO 文件
|
|
196
|
+
const todoPath = path.join(filePath, `${sessionId}.json`);
|
|
197
|
+
try {
|
|
198
|
+
await fs.unlink(todoPath);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
// 文件不存在,继续搜索
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
// 目录读取失败
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
112
212
|
}
|
|
113
213
|
/**
|
|
114
214
|
* 获取所有工具定义
|
|
@@ -325,7 +425,9 @@ Deleting a parent task automatically deletes all its subtasks (parentId relation
|
|
|
325
425
|
const todoItems = todos.map(t => {
|
|
326
426
|
const now = new Date().toISOString();
|
|
327
427
|
return {
|
|
328
|
-
id: `todo_${Date.now()}_${Math.random()
|
|
428
|
+
id: `todo_${Date.now()}_${Math.random()
|
|
429
|
+
.toString(36)
|
|
430
|
+
.slice(2, 9)}`,
|
|
329
431
|
content: t.content,
|
|
330
432
|
status: 'pending',
|
|
331
433
|
createdAt: now,
|
|
@@ -333,7 +435,8 @@ Deleting a parent task automatically deletes all its subtasks (parentId relation
|
|
|
333
435
|
parentId: t.parentId,
|
|
334
436
|
};
|
|
335
437
|
});
|
|
336
|
-
const
|
|
438
|
+
const existingList = await this.getTodoList(sessionId);
|
|
439
|
+
const result = await this.saveTodoList(sessionId, todoItems, existingList);
|
|
337
440
|
return {
|
|
338
441
|
content: [
|
|
339
442
|
{
|
|
@@ -349,7 +452,9 @@ Deleting a parent task automatically deletes all its subtasks (parentId relation
|
|
|
349
452
|
content: [
|
|
350
453
|
{
|
|
351
454
|
type: 'text',
|
|
352
|
-
text: result
|
|
455
|
+
text: result
|
|
456
|
+
? JSON.stringify(result, null, 2)
|
|
457
|
+
: 'No TODO list found',
|
|
353
458
|
},
|
|
354
459
|
],
|
|
355
460
|
};
|
|
@@ -366,7 +471,9 @@ Deleting a parent task automatically deletes all its subtasks (parentId relation
|
|
|
366
471
|
content: [
|
|
367
472
|
{
|
|
368
473
|
type: 'text',
|
|
369
|
-
text: result
|
|
474
|
+
text: result
|
|
475
|
+
? JSON.stringify(result, null, 2)
|
|
476
|
+
: 'TODO item not found',
|
|
370
477
|
},
|
|
371
478
|
],
|
|
372
479
|
};
|
|
@@ -390,7 +497,9 @@ Deleting a parent task automatically deletes all its subtasks (parentId relation
|
|
|
390
497
|
content: [
|
|
391
498
|
{
|
|
392
499
|
type: 'text',
|
|
393
|
-
text: result
|
|
500
|
+
text: result
|
|
501
|
+
? JSON.stringify(result, null, 2)
|
|
502
|
+
: 'TODO item not found',
|
|
394
503
|
},
|
|
395
504
|
],
|
|
396
505
|
};
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { useStdout } from 'ink';
|
|
3
|
+
import ansiEscapes from 'ansi-escapes';
|
|
4
|
+
import { highlight } from 'cli-highlight';
|
|
5
|
+
import { handleConversationWithTools } from '../../hooks/useConversation.js';
|
|
6
|
+
import { useStreamingState } from '../../hooks/useStreamingState.js';
|
|
7
|
+
import { useToolConfirmation } from '../../hooks/useToolConfirmation.js';
|
|
8
|
+
import { useVSCodeState } from '../../hooks/useVSCodeState.js';
|
|
9
|
+
import { parseAndValidateFileReferences, createMessageWithFileInstructions, getSystemInfo, } from '../../utils/fileUtils.js';
|
|
10
|
+
// Console-based markdown renderer functions
|
|
11
|
+
function renderConsoleMarkdown(content) {
|
|
12
|
+
const blocks = parseConsoleMarkdown(content);
|
|
13
|
+
return blocks.map(block => renderConsoleBlock(block)).join('\n');
|
|
14
|
+
}
|
|
15
|
+
function parseConsoleMarkdown(content) {
|
|
16
|
+
const blocks = [];
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < lines.length) {
|
|
20
|
+
const line = lines[i] ?? '';
|
|
21
|
+
// Check for code block
|
|
22
|
+
const codeBlockMatch = line.match(/^```(.*)$/);
|
|
23
|
+
if (codeBlockMatch) {
|
|
24
|
+
const language = codeBlockMatch[1]?.trim() || '';
|
|
25
|
+
const codeLines = [];
|
|
26
|
+
i++;
|
|
27
|
+
// Collect code block lines
|
|
28
|
+
while (i < lines.length) {
|
|
29
|
+
const currentLine = lines[i] ?? '';
|
|
30
|
+
if (currentLine.trim().startsWith('```')) {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
codeLines.push(currentLine);
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
blocks.push({
|
|
37
|
+
type: 'code',
|
|
38
|
+
language,
|
|
39
|
+
code: codeLines.join('\n'),
|
|
40
|
+
});
|
|
41
|
+
i++; // Skip closing ```
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// Check for heading
|
|
45
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
46
|
+
if (headingMatch) {
|
|
47
|
+
blocks.push({
|
|
48
|
+
type: 'heading',
|
|
49
|
+
level: headingMatch[1].length,
|
|
50
|
+
content: headingMatch[2].trim(),
|
|
51
|
+
});
|
|
52
|
+
i++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Check for list item
|
|
56
|
+
const listMatch = line.match(/^[\s]*[*\-]\s+(.+)$/);
|
|
57
|
+
if (listMatch) {
|
|
58
|
+
const listItems = [listMatch[1].trim()];
|
|
59
|
+
i++;
|
|
60
|
+
// Collect consecutive list items
|
|
61
|
+
while (i < lines.length) {
|
|
62
|
+
const currentLine = lines[i] ?? '';
|
|
63
|
+
const nextListMatch = currentLine.match(/^[\s]*[*\-]\s+(.+)$/);
|
|
64
|
+
if (!nextListMatch) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
listItems.push(nextListMatch[1].trim());
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
blocks.push({
|
|
71
|
+
type: 'list',
|
|
72
|
+
items: listItems,
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Collect text lines
|
|
77
|
+
const textLines = [];
|
|
78
|
+
while (i < lines.length) {
|
|
79
|
+
const currentLine = lines[i] ?? '';
|
|
80
|
+
if (currentLine.trim().startsWith('```') ||
|
|
81
|
+
currentLine.match(/^#{1,6}\s+/) ||
|
|
82
|
+
currentLine.match(/^[\s]*[*\-]\s+/)) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
textLines.push(currentLine);
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
if (textLines.length > 0) {
|
|
89
|
+
blocks.push({
|
|
90
|
+
type: 'text',
|
|
91
|
+
content: textLines.join('\n'),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return blocks;
|
|
96
|
+
}
|
|
97
|
+
function renderConsoleBlock(block) {
|
|
98
|
+
switch (block.type) {
|
|
99
|
+
case 'code': {
|
|
100
|
+
const highlightedCode = highlightConsoleCode(block.code, block.language);
|
|
101
|
+
const languageLabel = block.language
|
|
102
|
+
? `\x1b[42m\x1b[30m ${block.language} \x1b[0m`
|
|
103
|
+
: '';
|
|
104
|
+
return (`\n\x1b[90m┌─ Code Block\x1b[0m\n` +
|
|
105
|
+
(languageLabel ? `\x1b[90m│\x1b[0m ${languageLabel}\n` : '') +
|
|
106
|
+
`\x1b[90m├─\x1b[0m\n` +
|
|
107
|
+
`${highlightedCode}\n` +
|
|
108
|
+
`\x1b[90m└─ End of Code\x1b[0m`);
|
|
109
|
+
}
|
|
110
|
+
case 'heading': {
|
|
111
|
+
const headingColors = ['\x1b[96m', '\x1b[94m', '\x1b[95m', '\x1b[93m'];
|
|
112
|
+
const headingColor = headingColors[block.level - 1] || '\x1b[97m';
|
|
113
|
+
const prefix = '#'.repeat(block.level);
|
|
114
|
+
return `\n${headingColor}${prefix} ${renderInlineFormatting(block.content)}\x1b[0m`;
|
|
115
|
+
}
|
|
116
|
+
case 'list': {
|
|
117
|
+
return ('\n' +
|
|
118
|
+
block.items
|
|
119
|
+
.map((item) => `\x1b[93m•\x1b[0m ${renderInlineFormatting(item)}`)
|
|
120
|
+
.join('\n'));
|
|
121
|
+
}
|
|
122
|
+
case 'text': {
|
|
123
|
+
return ('\n' +
|
|
124
|
+
block.content
|
|
125
|
+
.split('\n')
|
|
126
|
+
.map((line) => line === '' ? '' : renderInlineFormatting(line))
|
|
127
|
+
.join('\n'));
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function highlightConsoleCode(code, language) {
|
|
134
|
+
try {
|
|
135
|
+
if (!language) {
|
|
136
|
+
return code
|
|
137
|
+
.split('\n')
|
|
138
|
+
.map(line => `\x1b[90m│ \x1b[37m${line}\x1b[0m`)
|
|
139
|
+
.join('\n');
|
|
140
|
+
}
|
|
141
|
+
// Map common language aliases
|
|
142
|
+
const languageMap = {
|
|
143
|
+
js: 'javascript',
|
|
144
|
+
ts: 'typescript',
|
|
145
|
+
py: 'python',
|
|
146
|
+
rb: 'ruby',
|
|
147
|
+
sh: 'bash',
|
|
148
|
+
shell: 'bash',
|
|
149
|
+
cs: 'csharp',
|
|
150
|
+
'c#': 'csharp',
|
|
151
|
+
cpp: 'cpp',
|
|
152
|
+
'c++': 'cpp',
|
|
153
|
+
yml: 'yaml',
|
|
154
|
+
md: 'markdown',
|
|
155
|
+
json: 'json',
|
|
156
|
+
xml: 'xml',
|
|
157
|
+
html: 'html',
|
|
158
|
+
css: 'css',
|
|
159
|
+
sql: 'sql',
|
|
160
|
+
java: 'java',
|
|
161
|
+
go: 'go',
|
|
162
|
+
rust: 'rust',
|
|
163
|
+
php: 'php',
|
|
164
|
+
};
|
|
165
|
+
const mappedLanguage = languageMap[language.toLowerCase()] || language.toLowerCase();
|
|
166
|
+
const highlighted = highlight(code, {
|
|
167
|
+
language: mappedLanguage,
|
|
168
|
+
ignoreIllegals: true,
|
|
169
|
+
});
|
|
170
|
+
return highlighted
|
|
171
|
+
.split('\n')
|
|
172
|
+
.map(line => `\x1b[90m│ \x1b[0m${line}`)
|
|
173
|
+
.join('\n');
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// If highlighting fails, return plain code
|
|
177
|
+
return code
|
|
178
|
+
.split('\n')
|
|
179
|
+
.map(line => `\x1b[90m│ \x1b[37m${line}\x1b[0m`)
|
|
180
|
+
.join('\n');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function renderInlineFormatting(text) {
|
|
184
|
+
// Handle inline code `code`
|
|
185
|
+
text = text.replace(/`([^`]+)`/g, (_, code) => {
|
|
186
|
+
return `\x1b[36m${code}\x1b[0m`;
|
|
187
|
+
});
|
|
188
|
+
// Handle bold **text** or __text__
|
|
189
|
+
text = text.replace(/(\*\*|__)([^*_]+)\1/g, (_, __, content) => {
|
|
190
|
+
return `\x1b[1m\x1b[97m${content}\x1b[0m`;
|
|
191
|
+
});
|
|
192
|
+
// Handle italic *text* or _text_
|
|
193
|
+
text = text.replace(/(?<!\*)(\*)(?!\*)([^*]+)\1(?!\*)/g, (_, __, content) => {
|
|
194
|
+
return `\x1b[3m\x1b[97m${content}\x1b[0m`;
|
|
195
|
+
});
|
|
196
|
+
return text;
|
|
197
|
+
}
|
|
198
|
+
export default function HeadlessModeScreen({ prompt, onComplete }) {
|
|
199
|
+
const [messages, setMessages] = useState([]);
|
|
200
|
+
const [isComplete, setIsComplete] = useState(false);
|
|
201
|
+
const { stdout } = useStdout();
|
|
202
|
+
// Use custom hooks
|
|
203
|
+
const streamingState = useStreamingState();
|
|
204
|
+
const vscodeState = useVSCodeState();
|
|
205
|
+
// Use tool confirmation hook
|
|
206
|
+
const { requestToolConfirmation, isToolAutoApproved, addMultipleToAlwaysApproved, } = useToolConfirmation();
|
|
207
|
+
// Listen for message changes to display AI responses and tool calls
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
const lastMessage = messages[messages.length - 1];
|
|
210
|
+
if (!lastMessage)
|
|
211
|
+
return;
|
|
212
|
+
if (lastMessage.role === 'assistant') {
|
|
213
|
+
if (lastMessage.toolPending) {
|
|
214
|
+
// Tool is being executed - use same icon as ChatScreen with colors
|
|
215
|
+
if (lastMessage.content.startsWith('⚡')) {
|
|
216
|
+
console.log(`\n\x1b[93m⚡ ${lastMessage.content}\x1b[0m`);
|
|
217
|
+
}
|
|
218
|
+
else if (lastMessage.content.startsWith('✓')) {
|
|
219
|
+
console.log(`\n\x1b[32m✓ ${lastMessage.content}\x1b[0m`);
|
|
220
|
+
}
|
|
221
|
+
else if (lastMessage.content.startsWith('✗')) {
|
|
222
|
+
console.log(`\n\x1b[31m✗ ${lastMessage.content}\x1b[0m`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
console.log(`\n\x1b[96m❆ ${lastMessage.content}\x1b[0m`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (lastMessage.content && !lastMessage.streaming) {
|
|
229
|
+
// Final response with markdown rendering and better formatting
|
|
230
|
+
console.log(renderConsoleMarkdown(lastMessage.content));
|
|
231
|
+
// Show tool results if available with better styling
|
|
232
|
+
if (lastMessage.toolCall &&
|
|
233
|
+
lastMessage.toolCall.name === 'terminal-execute') {
|
|
234
|
+
const args = lastMessage.toolCall.arguments;
|
|
235
|
+
if (args.command) {
|
|
236
|
+
console.log(`\n\x1b[90m┌─ Command\x1b[0m`);
|
|
237
|
+
console.log(`\x1b[33m│ ${args.command}\x1b[0m`);
|
|
238
|
+
}
|
|
239
|
+
if (args.stdout && args.stdout.trim()) {
|
|
240
|
+
console.log(`\x1b[90m├─ stdout\x1b[0m`);
|
|
241
|
+
const stdoutLines = args.stdout.split('\n');
|
|
242
|
+
stdoutLines.forEach((line) => {
|
|
243
|
+
console.log(`\x1b[90m│ \x1b[32m${line}\x1b[0m`);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (args.stderr && args.stderr.trim()) {
|
|
247
|
+
console.log(`\x1b[90m├─ stderr\x1b[0m`);
|
|
248
|
+
const stderrLines = args.stderr.split('\n');
|
|
249
|
+
stderrLines.forEach((line) => {
|
|
250
|
+
console.log(`\x1b[90m│ \x1b[31m${line}\x1b[0m`);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (args.command || args.stdout || args.stderr) {
|
|
254
|
+
console.log(`\x1b[90m└─ Execution complete\x1b[0m`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}, [messages]);
|
|
260
|
+
// Listen for streaming state to show loading status
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (streamingState.isStreaming) {
|
|
263
|
+
if (streamingState.retryStatus && streamingState.retryStatus.isRetrying) {
|
|
264
|
+
// Show retry status with colors
|
|
265
|
+
if (streamingState.retryStatus.errorMessage) {
|
|
266
|
+
console.log(`\n\x1b[31m✗ Error: ${streamingState.retryStatus.errorMessage}\x1b[0m`);
|
|
267
|
+
}
|
|
268
|
+
if (streamingState.retryStatus.remainingSeconds !== undefined &&
|
|
269
|
+
streamingState.retryStatus.remainingSeconds > 0) {
|
|
270
|
+
console.log(`\n\x1b[93m⟳ Retry \x1b[33m${streamingState.retryStatus.attempt}/5\x1b[93m in \x1b[32m${streamingState.retryStatus.remainingSeconds}s\x1b[93m...\x1b[0m`);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
console.log(`\n\x1b[93m⟳ Resending... \x1b[33m(Attempt ${streamingState.retryStatus.attempt}/5)\x1b[0m`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// Show normal thinking status with colors
|
|
278
|
+
const thinkingText = streamingState.isReasoning
|
|
279
|
+
? 'Deep thinking...'
|
|
280
|
+
: 'Thinking...';
|
|
281
|
+
process.stdout.write(`\r\x1b[96m❆\x1b[90m ${thinkingText} \x1b[37m(\x1b[33m${streamingState.elapsedSeconds}s\x1b[37m · \x1b[32m↓ ${streamingState.streamTokenCount} tokens\x1b[37m)\x1b[0m`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}, [
|
|
285
|
+
streamingState.isStreaming,
|
|
286
|
+
streamingState.isReasoning,
|
|
287
|
+
streamingState.elapsedSeconds,
|
|
288
|
+
streamingState.streamTokenCount,
|
|
289
|
+
streamingState.retryStatus,
|
|
290
|
+
]);
|
|
291
|
+
const processMessage = async () => {
|
|
292
|
+
try {
|
|
293
|
+
// Parse and validate file references
|
|
294
|
+
const { cleanContent, validFiles } = await parseAndValidateFileReferences(prompt);
|
|
295
|
+
const regularFiles = validFiles.filter(f => !f.isImage);
|
|
296
|
+
// Get system information
|
|
297
|
+
const systemInfo = getSystemInfo();
|
|
298
|
+
// Add user message to UI
|
|
299
|
+
const userMessage = {
|
|
300
|
+
role: 'user',
|
|
301
|
+
content: cleanContent,
|
|
302
|
+
files: validFiles.length > 0 ? validFiles : undefined,
|
|
303
|
+
systemInfo,
|
|
304
|
+
};
|
|
305
|
+
setMessages([userMessage]);
|
|
306
|
+
streamingState.setIsStreaming(true);
|
|
307
|
+
// Create new abort controller for this request
|
|
308
|
+
const controller = new AbortController();
|
|
309
|
+
streamingState.setAbortController(controller);
|
|
310
|
+
// Clear terminal and start headless output
|
|
311
|
+
stdout.write(ansiEscapes.clearTerminal);
|
|
312
|
+
// Print colorful banner
|
|
313
|
+
console.log(`\x1b[94m╭─────────────────────────────────────────────────────────╮\x1b[0m`);
|
|
314
|
+
console.log(`\x1b[94m│\x1b[96m ❆ Snow AI CLI - Headless Mode ❆ \x1b[94m│\x1b[0m`);
|
|
315
|
+
console.log(`\x1b[94m╰─────────────────────────────────────────────────────────╯\x1b[0m`);
|
|
316
|
+
// Print user prompt with styling
|
|
317
|
+
console.log(`\n\x1b[36m┌─ User Query\x1b[0m`);
|
|
318
|
+
console.log(`\x1b[97m│ ${cleanContent}\x1b[0m`);
|
|
319
|
+
if (validFiles.length > 0) {
|
|
320
|
+
console.log(`\x1b[36m├─ Files\x1b[0m`);
|
|
321
|
+
validFiles.forEach(file => {
|
|
322
|
+
const statusColor = file.exists ? '\x1b[32m' : '\x1b[31m';
|
|
323
|
+
const statusText = file.exists ? '✓' : '✗';
|
|
324
|
+
console.log(`\x1b[90m│ └─ ${statusColor}${statusText}\x1b[90m ${file.path}${file.exists
|
|
325
|
+
? `\x1b[33m (${file.lineCount} lines)\x1b[90m`
|
|
326
|
+
: '\x1b[31m (not found)\x1b[90m'}\x1b[0m`);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (systemInfo) {
|
|
330
|
+
console.log(`\x1b[36m├─ System Context\x1b[0m`);
|
|
331
|
+
console.log(`\x1b[90m│ └─ Platform: \x1b[33m${systemInfo.platform}\x1b[0m`);
|
|
332
|
+
console.log(`\x1b[90m│ └─ Shell: \x1b[33m${systemInfo.shell}\x1b[0m`);
|
|
333
|
+
console.log(`\x1b[90m│ └─ Working Directory: \x1b[33m${systemInfo.workingDirectory}\x1b[0m`);
|
|
334
|
+
}
|
|
335
|
+
console.log(`\x1b[36m└─ Assistant Response\x1b[0m`);
|
|
336
|
+
// Create message for AI
|
|
337
|
+
const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, systemInfo, vscodeState.vscodeConnected ? vscodeState.editorContext : undefined);
|
|
338
|
+
// Custom save message function for headless mode
|
|
339
|
+
const saveMessage = async () => {
|
|
340
|
+
// In headless mode, we don't need to save messages
|
|
341
|
+
};
|
|
342
|
+
// Start conversation with tool support
|
|
343
|
+
await handleConversationWithTools({
|
|
344
|
+
userContent: messageForAI,
|
|
345
|
+
imageContents: [],
|
|
346
|
+
controller,
|
|
347
|
+
messages,
|
|
348
|
+
saveMessage,
|
|
349
|
+
setMessages,
|
|
350
|
+
setStreamTokenCount: streamingState.setStreamTokenCount,
|
|
351
|
+
setCurrentTodos: () => { }, // No-op in headless mode
|
|
352
|
+
requestToolConfirmation,
|
|
353
|
+
isToolAutoApproved,
|
|
354
|
+
addMultipleToAlwaysApproved,
|
|
355
|
+
yoloMode: true, // Always use YOLO mode in headless
|
|
356
|
+
setContextUsage: streamingState.setContextUsage,
|
|
357
|
+
useBasicModel: false,
|
|
358
|
+
getPendingMessages: () => [],
|
|
359
|
+
clearPendingMessages: () => { },
|
|
360
|
+
setIsStreaming: streamingState.setIsStreaming,
|
|
361
|
+
setIsReasoning: streamingState.setIsReasoning,
|
|
362
|
+
setRetryStatus: streamingState.setRetryStatus,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
console.error(`\n\x1b[31m✗ Error:\x1b[0m`, error instanceof Error
|
|
367
|
+
? `\x1b[91m${error.message}\x1b[0m`
|
|
368
|
+
: '\x1b[91mUnknown error occurred\x1b[0m');
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
// End streaming
|
|
372
|
+
streamingState.setIsStreaming(false);
|
|
373
|
+
streamingState.setAbortController(null);
|
|
374
|
+
streamingState.setStreamTokenCount(0);
|
|
375
|
+
setIsComplete(true);
|
|
376
|
+
// Wait a moment then call onComplete
|
|
377
|
+
setTimeout(() => {
|
|
378
|
+
onComplete();
|
|
379
|
+
}, 1000);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
processMessage();
|
|
384
|
+
}, []);
|
|
385
|
+
// Simple console output mode - don't render anything
|
|
386
|
+
if (isComplete) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
// Return empty fragment - we're using console.log for output
|
|
390
|
+
return React.createElement(React.Fragment, null);
|
|
391
|
+
}
|
|
@@ -27,6 +27,7 @@ declare class SessionManager {
|
|
|
27
27
|
constructor();
|
|
28
28
|
private ensureSessionsDir;
|
|
29
29
|
private getSessionPath;
|
|
30
|
+
private formatDateForFolder;
|
|
30
31
|
/**
|
|
31
32
|
* Cancel any ongoing summary generation
|
|
32
33
|
* This prevents wasted resources and race conditions
|
|
@@ -35,7 +36,9 @@ declare class SessionManager {
|
|
|
35
36
|
createNewSession(): Promise<Session>;
|
|
36
37
|
saveSession(session: Session): Promise<void>;
|
|
37
38
|
loadSession(sessionId: string): Promise<Session | null>;
|
|
39
|
+
private findSessionInDateFolders;
|
|
38
40
|
listSessions(): Promise<SessionListItem[]>;
|
|
41
|
+
private readSessionsFromDir;
|
|
39
42
|
addMessage(message: ChatMessage): Promise<void>;
|
|
40
43
|
getCurrentSession(): Session | null;
|
|
41
44
|
setCurrentSession(session: Session): void;
|
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
5
5
|
import { summaryAgent } from '../agents/summaryAgent.js';
|
|
6
|
+
import { getTodoService } from './mcpToolsManager.js';
|
|
6
7
|
class SessionManager {
|
|
7
8
|
constructor() {
|
|
8
9
|
Object.defineProperty(this, "sessionsDir", {
|
|
@@ -31,16 +32,30 @@ class SessionManager {
|
|
|
31
32
|
});
|
|
32
33
|
this.sessionsDir = path.join(os.homedir(), '.snow', 'sessions');
|
|
33
34
|
}
|
|
34
|
-
async ensureSessionsDir() {
|
|
35
|
+
async ensureSessionsDir(date) {
|
|
35
36
|
try {
|
|
36
37
|
await fs.mkdir(this.sessionsDir, { recursive: true });
|
|
38
|
+
if (date) {
|
|
39
|
+
const dateFolder = this.formatDateForFolder(date);
|
|
40
|
+
const sessionDir = path.join(this.sessionsDir, dateFolder);
|
|
41
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
catch (error) {
|
|
39
45
|
// Directory already exists or other error
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
|
-
getSessionPath(sessionId) {
|
|
43
|
-
|
|
48
|
+
getSessionPath(sessionId, date) {
|
|
49
|
+
const sessionDate = date || new Date();
|
|
50
|
+
const dateFolder = this.formatDateForFolder(sessionDate);
|
|
51
|
+
const sessionDir = path.join(this.sessionsDir, dateFolder);
|
|
52
|
+
return path.join(sessionDir, `${sessionId}.json`);
|
|
53
|
+
}
|
|
54
|
+
formatDateForFolder(date) {
|
|
55
|
+
const year = date.getFullYear();
|
|
56
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
57
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
58
|
+
return `${year}-${month}-${day}`;
|
|
44
59
|
}
|
|
45
60
|
/**
|
|
46
61
|
* Cancel any ongoing summary generation
|
|
@@ -57,7 +72,7 @@ class SessionManager {
|
|
|
57
72
|
}
|
|
58
73
|
}
|
|
59
74
|
async createNewSession() {
|
|
60
|
-
await this.ensureSessionsDir();
|
|
75
|
+
await this.ensureSessionsDir(new Date());
|
|
61
76
|
// 使用 UUID v4 生成唯一会话 ID,避免并发冲突
|
|
62
77
|
const sessionId = randomUUID();
|
|
63
78
|
const session = {
|
|
@@ -74,32 +89,79 @@ class SessionManager {
|
|
|
74
89
|
return session;
|
|
75
90
|
}
|
|
76
91
|
async saveSession(session) {
|
|
77
|
-
|
|
78
|
-
|
|
92
|
+
const sessionDate = new Date(session.createdAt);
|
|
93
|
+
await this.ensureSessionsDir(sessionDate);
|
|
94
|
+
const sessionPath = this.getSessionPath(session.id, sessionDate);
|
|
79
95
|
await fs.writeFile(sessionPath, JSON.stringify(session, null, 2));
|
|
80
96
|
}
|
|
81
97
|
async loadSession(sessionId) {
|
|
98
|
+
// 首先尝试从旧格式加载(向下兼容)
|
|
82
99
|
try {
|
|
83
|
-
const
|
|
84
|
-
const data = await fs.readFile(
|
|
100
|
+
const oldSessionPath = path.join(this.sessionsDir, `${sessionId}.json`);
|
|
101
|
+
const data = await fs.readFile(oldSessionPath, 'utf-8');
|
|
85
102
|
const session = JSON.parse(data);
|
|
86
103
|
this.currentSession = session;
|
|
87
104
|
return session;
|
|
88
105
|
}
|
|
89
106
|
catch (error) {
|
|
90
|
-
|
|
107
|
+
// 旧格式不存在,搜索日期文件夹
|
|
108
|
+
}
|
|
109
|
+
// 在日期文件夹中查找会话
|
|
110
|
+
try {
|
|
111
|
+
const session = await this.findSessionInDateFolders(sessionId);
|
|
112
|
+
if (session) {
|
|
113
|
+
this.currentSession = session;
|
|
114
|
+
return session;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
// 搜索失败
|
|
91
119
|
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
async findSessionInDateFolders(sessionId) {
|
|
123
|
+
try {
|
|
124
|
+
const files = await fs.readdir(this.sessionsDir);
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const filePath = path.join(this.sessionsDir, file);
|
|
127
|
+
const stat = await fs.stat(filePath);
|
|
128
|
+
if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
|
|
129
|
+
// 这是日期文件夹,查找会话文件
|
|
130
|
+
const sessionPath = path.join(filePath, `${sessionId}.json`);
|
|
131
|
+
try {
|
|
132
|
+
const data = await fs.readFile(sessionPath, 'utf-8');
|
|
133
|
+
const session = JSON.parse(data);
|
|
134
|
+
return session;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
// 文件不存在或读取失败,继续搜索
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
// 目录读取失败
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
92
147
|
}
|
|
93
148
|
async listSessions() {
|
|
94
149
|
await this.ensureSessionsDir();
|
|
150
|
+
const sessions = [];
|
|
95
151
|
try {
|
|
152
|
+
// 首先处理新的日期文件夹结构
|
|
96
153
|
const files = await fs.readdir(this.sessionsDir);
|
|
97
|
-
const sessions = [];
|
|
98
154
|
for (const file of files) {
|
|
99
|
-
|
|
155
|
+
const filePath = path.join(this.sessionsDir, file);
|
|
156
|
+
const stat = await fs.stat(filePath);
|
|
157
|
+
if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
|
|
158
|
+
// 这是日期文件夹,读取其中的会话文件
|
|
159
|
+
await this.readSessionsFromDir(filePath, sessions);
|
|
160
|
+
}
|
|
161
|
+
else if (file.endsWith('.json')) {
|
|
162
|
+
// 这是旧格式的会话文件(向下兼容)
|
|
100
163
|
try {
|
|
101
|
-
const
|
|
102
|
-
const data = await fs.readFile(sessionPath, 'utf-8');
|
|
164
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
103
165
|
const session = JSON.parse(data);
|
|
104
166
|
sessions.push({
|
|
105
167
|
id: session.id,
|
|
@@ -123,6 +185,35 @@ class SessionManager {
|
|
|
123
185
|
return [];
|
|
124
186
|
}
|
|
125
187
|
}
|
|
188
|
+
async readSessionsFromDir(dirPath, sessions) {
|
|
189
|
+
try {
|
|
190
|
+
const files = await fs.readdir(dirPath);
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
if (file.endsWith('.json')) {
|
|
193
|
+
try {
|
|
194
|
+
const sessionPath = path.join(dirPath, file);
|
|
195
|
+
const data = await fs.readFile(sessionPath, 'utf-8');
|
|
196
|
+
const session = JSON.parse(data);
|
|
197
|
+
sessions.push({
|
|
198
|
+
id: session.id,
|
|
199
|
+
title: session.title,
|
|
200
|
+
summary: session.summary,
|
|
201
|
+
createdAt: session.createdAt,
|
|
202
|
+
updatedAt: session.updatedAt,
|
|
203
|
+
messageCount: session.messageCount,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// Skip invalid session files
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
// Skip directory if it can't be read
|
|
215
|
+
}
|
|
216
|
+
}
|
|
126
217
|
async addMessage(message) {
|
|
127
218
|
if (!this.currentSession) {
|
|
128
219
|
this.currentSession = await this.createNewSession();
|
|
@@ -239,14 +330,54 @@ class SessionManager {
|
|
|
239
330
|
this.currentSession = null;
|
|
240
331
|
}
|
|
241
332
|
async deleteSession(sessionId) {
|
|
333
|
+
let sessionDeleted = false;
|
|
334
|
+
// 首先尝试删除旧格式(向下兼容)
|
|
242
335
|
try {
|
|
243
|
-
const
|
|
244
|
-
await fs.unlink(
|
|
245
|
-
|
|
336
|
+
const oldSessionPath = path.join(this.sessionsDir, `${sessionId}.json`);
|
|
337
|
+
await fs.unlink(oldSessionPath);
|
|
338
|
+
sessionDeleted = true;
|
|
246
339
|
}
|
|
247
340
|
catch (error) {
|
|
248
|
-
|
|
341
|
+
// 旧格式不存在,搜索日期文件夹
|
|
342
|
+
}
|
|
343
|
+
// 在日期文件夹中查找并删除会话
|
|
344
|
+
if (!sessionDeleted) {
|
|
345
|
+
try {
|
|
346
|
+
const files = await fs.readdir(this.sessionsDir);
|
|
347
|
+
for (const file of files) {
|
|
348
|
+
const filePath = path.join(this.sessionsDir, file);
|
|
349
|
+
const stat = await fs.stat(filePath);
|
|
350
|
+
if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
|
|
351
|
+
// 这是日期文件夹,查找会话文件
|
|
352
|
+
const sessionPath = path.join(filePath, `${sessionId}.json`);
|
|
353
|
+
try {
|
|
354
|
+
await fs.unlink(sessionPath);
|
|
355
|
+
sessionDeleted = true;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
// 文件不存在,继续搜索
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
// 目录读取失败
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// 如果会话删除成功,同时删除对应的TODO列表
|
|
370
|
+
if (sessionDeleted) {
|
|
371
|
+
try {
|
|
372
|
+
const todoService = getTodoService();
|
|
373
|
+
await todoService.deleteTodoList(sessionId);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
// TODO删除失败不影响会话删除结果
|
|
377
|
+
console.warn(`Failed to delete TODO list for session ${sessionId}:`, error);
|
|
378
|
+
}
|
|
249
379
|
}
|
|
380
|
+
return sessionDeleted;
|
|
250
381
|
}
|
|
251
382
|
async truncateMessages(messageCount) {
|
|
252
383
|
if (!this.currentSession) {
|