snow-ai 0.3.2 → 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/agents/summaryAgent.d.ts +31 -0
- package/dist/agents/summaryAgent.js +256 -0
- 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/hooks/useSessionSave.js +13 -2
- package/dist/mcp/todo.d.ts +8 -1
- package/dist/mcp/todo.js +126 -17
- package/dist/ui/pages/ChatScreen.js +12 -5
- package/dist/ui/pages/HeadlessModeScreen.d.ts +7 -0
- package/dist/ui/pages/HeadlessModeScreen.js +391 -0
- package/dist/utils/sessionManager.d.ts +10 -0
- package/dist/utils/sessionManager.js +231 -20
- package/package.json +1 -1
- package/readme.md +78 -57
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
|
};
|
|
@@ -674,11 +674,18 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
674
674
|
.filter(m => m.toolPending)
|
|
675
675
|
.map((message, index) => (React.createElement(Box, { key: `pending-tool-${index}`, marginBottom: 1, paddingX: 1, width: terminalWidth },
|
|
676
676
|
React.createElement(Text, { color: "yellowBright", bold: true }, "\u2746"),
|
|
677
|
-
React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "
|
|
678
|
-
React.createElement(
|
|
679
|
-
|
|
680
|
-
React.createElement(
|
|
681
|
-
React.createElement(
|
|
677
|
+
React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "column" },
|
|
678
|
+
React.createElement(Box, { flexDirection: "row" },
|
|
679
|
+
React.createElement(MarkdownRenderer, { content: message.content || ' ', color: "yellow" }),
|
|
680
|
+
React.createElement(Box, { marginLeft: 1 },
|
|
681
|
+
React.createElement(Text, { color: "yellow" },
|
|
682
|
+
React.createElement(Spinner, { type: "dots" })))),
|
|
683
|
+
message.toolDisplay && message.toolDisplay.args.length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 }, message.toolDisplay.args.map((arg, argIndex) => (React.createElement(Text, { key: argIndex, color: "gray", dimColor: true },
|
|
684
|
+
arg.isLast ? '└─' : '├─',
|
|
685
|
+
" ",
|
|
686
|
+
arg.key,
|
|
687
|
+
": ",
|
|
688
|
+
arg.value))))))))),
|
|
682
689
|
(streamingState.isStreaming || isSaving) && !pendingToolConfirmation && (React.createElement(Box, { marginBottom: 1, paddingX: 1, width: terminalWidth },
|
|
683
690
|
React.createElement(Text, { color: ['#FF6EBF', 'green', 'blue', 'cyan', '#B588F8'][streamingState.animationFrame], bold: true }, "\u2746"),
|
|
684
691
|
React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "column" }, streamingState.isStreaming ? (React.createElement(React.Fragment, null, streamingState.retryStatus &&
|
|
@@ -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
|
+
}
|
|
@@ -22,13 +22,23 @@ export interface SessionListItem {
|
|
|
22
22
|
declare class SessionManager {
|
|
23
23
|
private readonly sessionsDir;
|
|
24
24
|
private currentSession;
|
|
25
|
+
private summaryAbortController;
|
|
26
|
+
private summaryTimeoutId;
|
|
25
27
|
constructor();
|
|
26
28
|
private ensureSessionsDir;
|
|
27
29
|
private getSessionPath;
|
|
30
|
+
private formatDateForFolder;
|
|
31
|
+
/**
|
|
32
|
+
* Cancel any ongoing summary generation
|
|
33
|
+
* This prevents wasted resources and race conditions
|
|
34
|
+
*/
|
|
35
|
+
private cancelOngoingSummaryGeneration;
|
|
28
36
|
createNewSession(): Promise<Session>;
|
|
29
37
|
saveSession(session: Session): Promise<void>;
|
|
30
38
|
loadSession(sessionId: string): Promise<Session | null>;
|
|
39
|
+
private findSessionInDateFolders;
|
|
31
40
|
listSessions(): Promise<SessionListItem[]>;
|
|
41
|
+
private readSessionsFromDir;
|
|
32
42
|
addMessage(message: ChatMessage): Promise<void>;
|
|
33
43
|
getCurrentSession(): Session | null;
|
|
34
44
|
setCurrentSession(session: Session): void;
|