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/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
- return path.join(this.todoDir, `${sessionId}.json`);
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
- const todoPath = this.getTodoPath(sessionId);
34
- let existingList;
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
- const todoPath = this.getTodoPath(sessionId);
83
+ // 首先尝试从旧格式加载(向下兼容)
57
84
  try {
58
- const content = await fs.readFile(todoPath, 'utf-8');
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
- return null;
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().toString(36).slice(2, 9)}`,
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 result = await this.saveTodoList(sessionId, todoItems);
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 ? JSON.stringify(result, null, 2) : 'No TODO list found',
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 ? JSON.stringify(result, null, 2) : 'TODO item not found',
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 ? JSON.stringify(result, null, 2) : 'TODO item not found',
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: "row" },
678
- React.createElement(MarkdownRenderer, { content: message.content || ' ', color: "yellow" }),
679
- React.createElement(Box, { marginLeft: 1 },
680
- React.createElement(Text, { color: "yellow" },
681
- React.createElement(Spinner, { type: "dots" }))))))),
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,7 @@
1
+ import React from 'react';
2
+ type Props = {
3
+ prompt: string;
4
+ onComplete: () => void;
5
+ };
6
+ export default function HeadlessModeScreen({ prompt, onComplete }: Props): React.JSX.Element | null;
7
+ export {};
@@ -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;