snow-ai 0.3.4 → 0.3.6

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.
Files changed (37) hide show
  1. package/dist/api/anthropic.js +38 -13
  2. package/dist/api/types.d.ts +1 -0
  3. package/dist/hooks/useConversation.js +226 -59
  4. package/dist/hooks/useSnapshotState.d.ts +2 -0
  5. package/dist/hooks/useToolConfirmation.js +1 -1
  6. package/dist/mcp/subagent.d.ts +35 -0
  7. package/dist/mcp/subagent.js +64 -0
  8. package/dist/ui/components/ChatInput.d.ts +1 -2
  9. package/dist/ui/components/ChatInput.js +47 -39
  10. package/dist/ui/components/FileRollbackConfirmation.d.ts +3 -2
  11. package/dist/ui/components/FileRollbackConfirmation.js +81 -22
  12. package/dist/ui/components/MessageList.d.ts +7 -1
  13. package/dist/ui/components/MessageList.js +16 -5
  14. package/dist/ui/components/ToolResultPreview.js +21 -1
  15. package/dist/ui/pages/ChatScreen.js +29 -20
  16. package/dist/ui/pages/ConfigScreen.js +47 -46
  17. package/dist/ui/pages/ProxyConfigScreen.js +1 -1
  18. package/dist/ui/pages/SubAgentConfigScreen.d.ts +9 -0
  19. package/dist/ui/pages/SubAgentConfigScreen.js +352 -0
  20. package/dist/ui/pages/SubAgentListScreen.d.ts +9 -0
  21. package/dist/ui/pages/SubAgentListScreen.js +114 -0
  22. package/dist/ui/pages/WelcomeScreen.js +30 -2
  23. package/dist/utils/incrementalSnapshot.d.ts +7 -0
  24. package/dist/utils/incrementalSnapshot.js +34 -0
  25. package/dist/utils/mcpToolsManager.js +41 -1
  26. package/dist/utils/retryUtils.js +5 -0
  27. package/dist/utils/sessionConverter.d.ts +1 -0
  28. package/dist/utils/sessionConverter.js +192 -89
  29. package/dist/utils/subAgentConfig.d.ts +43 -0
  30. package/dist/utils/subAgentConfig.js +126 -0
  31. package/dist/utils/subAgentExecutor.d.ts +29 -0
  32. package/dist/utils/subAgentExecutor.js +272 -0
  33. package/dist/utils/toolExecutor.d.ts +10 -2
  34. package/dist/utils/toolExecutor.js +46 -5
  35. package/package.json +1 -1
  36. package/dist/ui/pages/ConfigProfileScreen.d.ts +0 -7
  37. package/dist/ui/pages/ConfigProfileScreen.js +0 -300
@@ -70,6 +70,11 @@ function isRetriableError(error) {
70
70
  errorMessage.includes('socket hang up')) {
71
71
  return true;
72
72
  }
73
+ // JSON parsing errors from streaming (incomplete or malformed tool calls)
74
+ if (errorMessage.includes('invalid tool call json') ||
75
+ errorMessage.includes('incomplete tool call json')) {
76
+ return true;
77
+ }
73
78
  return false;
74
79
  }
75
80
  /**
@@ -2,5 +2,6 @@ import type { ChatMessage } from '../api/chat.js';
2
2
  import type { Message } from '../ui/components/MessageList.js';
3
3
  /**
4
4
  * Convert API format session messages to UI format messages
5
+ * Process messages in order to maintain correct sequence
5
6
  */
6
7
  export declare function convertSessionMessagesToUI(sessionMessages: ChatMessage[]): Message[];
@@ -1,34 +1,21 @@
1
1
  import { formatToolCallMessage } from './messageFormatter.js';
2
2
  /**
3
3
  * Convert API format session messages to UI format messages
4
+ * Process messages in order to maintain correct sequence
4
5
  */
5
6
  export function convertSessionMessagesToUI(sessionMessages) {
6
7
  const uiMessages = [];
7
- // First pass: build a map of tool_call_id to tool results
8
- const toolResultsMap = new Map();
9
- for (const msg of sessionMessages) {
10
- if (msg.role === 'tool' && msg.tool_call_id) {
11
- toolResultsMap.set(msg.tool_call_id, msg.content);
12
- }
13
- }
14
- for (const msg of sessionMessages) {
8
+ // Track which tool_calls have been processed
9
+ const processedToolCalls = new Set();
10
+ for (let i = 0; i < sessionMessages.length; i++) {
11
+ const msg = sessionMessages[i];
12
+ if (!msg)
13
+ continue;
15
14
  // Skip system messages
16
15
  if (msg.role === 'system')
17
16
  continue;
18
- // Skip tool role messages (we'll attach them to tool calls)
19
- if (msg.role === 'tool')
20
- continue;
21
- // Handle user and assistant messages
22
- const uiMessage = {
23
- role: msg.role,
24
- content: msg.content,
25
- streaming: false,
26
- images: msg.images,
27
- };
28
- // If assistant message has tool_calls, expand to show each tool call
29
- if (msg.role === 'assistant' &&
30
- msg.tool_calls &&
31
- msg.tool_calls.length > 0) {
17
+ // Handle sub-agent internal tool call messages
18
+ if (msg.subAgentInternal && msg.role === 'assistant' && msg.tool_calls) {
32
19
  for (const toolCall of msg.tool_calls) {
33
20
  const toolDisplay = formatToolCallMessage(toolCall);
34
21
  let toolArgs;
@@ -38,59 +25,96 @@ export function convertSessionMessagesToUI(sessionMessages) {
38
25
  catch (e) {
39
26
  toolArgs = {};
40
27
  }
41
- // Get the tool result for this tool call
42
- const toolResult = toolResultsMap.get(toolCall.id);
43
- const isError = toolResult?.startsWith('Error:') || false;
44
- // For filesystem-edit and filesystem-edit_search, try to extract diff data from result
45
- let editDiffData;
46
- if ((toolCall.function.name === 'filesystem-edit' || toolCall.function.name === 'filesystem-edit_search') &&
47
- toolResult &&
48
- !isError) {
49
- try {
50
- const resultData = JSON.parse(toolResult);
51
- if (resultData.oldContent && resultData.newContent) {
52
- editDiffData = {
53
- oldContent: resultData.oldContent,
54
- newContent: resultData.newContent,
55
- filename: toolArgs.filePath,
56
- completeOldContent: resultData.completeOldContent,
57
- completeNewContent: resultData.completeNewContent,
58
- contextStartLine: resultData.contextStartLine
59
- };
60
- // Merge diff data into toolArgs for DiffViewer
61
- toolArgs.oldContent = resultData.oldContent;
62
- toolArgs.newContent = resultData.newContent;
63
- toolArgs.completeOldContent = resultData.completeOldContent;
64
- toolArgs.completeNewContent = resultData.completeNewContent;
65
- toolArgs.contextStartLine = resultData.contextStartLine;
28
+ uiMessages.push({
29
+ role: 'subagent',
30
+ content: `\x1b[38;2;184;122;206m⚇⚡ ${toolDisplay.toolName}\x1b[0m`,
31
+ streaming: false,
32
+ toolCall: {
33
+ name: toolCall.function.name,
34
+ arguments: toolArgs,
35
+ },
36
+ toolDisplay,
37
+ toolCallId: toolCall.id,
38
+ toolPending: false,
39
+ subAgentInternal: true,
40
+ });
41
+ processedToolCalls.add(toolCall.id);
42
+ }
43
+ continue;
44
+ }
45
+ // Handle sub-agent internal tool result messages
46
+ if (msg.subAgentInternal && msg.role === 'tool' && msg.tool_call_id) {
47
+ const isError = msg.content.startsWith('Error:');
48
+ const statusIcon = isError ? '✗' : '✓';
49
+ const statusText = isError ? `\n └─ ${msg.content}` : '';
50
+ // Find tool name from previous assistant message
51
+ let toolName = 'tool';
52
+ let terminalResultData;
53
+ for (let j = i - 1; j >= 0; j--) {
54
+ const prevMsg = sessionMessages[j];
55
+ if (!prevMsg)
56
+ continue;
57
+ if (prevMsg.role === 'assistant' &&
58
+ prevMsg.tool_calls &&
59
+ prevMsg.subAgentInternal) {
60
+ const tc = prevMsg.tool_calls.find(t => t.id === msg.tool_call_id);
61
+ if (tc) {
62
+ toolName = tc.function.name;
63
+ if (toolName === 'terminal-execute' && !isError) {
64
+ try {
65
+ const resultData = JSON.parse(msg.content);
66
+ if (resultData.stdout !== undefined ||
67
+ resultData.stderr !== undefined) {
68
+ terminalResultData = {
69
+ stdout: resultData.stdout,
70
+ stderr: resultData.stderr,
71
+ exitCode: resultData.exitCode,
72
+ command: resultData.command,
73
+ };
74
+ }
75
+ }
76
+ catch (e) {
77
+ // Ignore parse errors
78
+ }
66
79
  }
67
- }
68
- catch (e) {
69
- // If parsing fails, just show regular result
80
+ break;
70
81
  }
71
82
  }
72
- // For terminal-execute, try to extract terminal result data
73
- let terminalResultData;
74
- if (toolCall.function.name === 'terminal-execute' &&
75
- toolResult &&
76
- !isError) {
77
- try {
78
- const resultData = JSON.parse(toolResult);
79
- if (resultData.stdout !== undefined ||
80
- resultData.stderr !== undefined) {
81
- terminalResultData = {
82
- stdout: resultData.stdout,
83
- stderr: resultData.stderr,
84
- exitCode: resultData.exitCode,
85
- command: toolArgs.command,
86
- };
87
- }
88
- }
89
- catch (e) {
90
- // If parsing fails, just show regular result
83
+ }
84
+ uiMessages.push({
85
+ role: 'subagent',
86
+ content: `\x1b[38;2;0;186;255m⚇${statusIcon} ${toolName}\x1b[0m${statusText}`,
87
+ streaming: false,
88
+ toolResult: !isError ? msg.content : undefined,
89
+ terminalResult: terminalResultData,
90
+ toolCall: terminalResultData
91
+ ? {
92
+ name: toolName,
93
+ arguments: terminalResultData,
91
94
  }
95
+ : undefined,
96
+ subAgentInternal: true,
97
+ });
98
+ continue;
99
+ }
100
+ // Handle regular assistant messages with tool_calls
101
+ if (msg.role === 'assistant' &&
102
+ msg.tool_calls &&
103
+ msg.tool_calls.length > 0 &&
104
+ !msg.subAgentInternal) {
105
+ for (const toolCall of msg.tool_calls) {
106
+ // Skip if already processed
107
+ if (processedToolCalls.has(toolCall.id))
108
+ continue;
109
+ const toolDisplay = formatToolCallMessage(toolCall);
110
+ let toolArgs;
111
+ try {
112
+ toolArgs = JSON.parse(toolCall.function.arguments);
92
113
  }
93
- // Create tool call message
114
+ catch (e) {
115
+ toolArgs = {};
116
+ }
117
+ // Add tool call message
94
118
  uiMessages.push({
95
119
  role: 'assistant',
96
120
  content: `⚡ ${toolDisplay.toolName}`,
@@ -101,29 +125,108 @@ export function convertSessionMessagesToUI(sessionMessages) {
101
125
  },
102
126
  toolDisplay,
103
127
  });
104
- // Create tool result message
105
- if (toolResult) {
106
- const statusIcon = isError ? '✗' : '✓';
107
- const statusText = isError ? `\n └─ ${toolResult}` : '';
108
- uiMessages.push({
109
- role: 'assistant',
110
- content: `${statusIcon} ${toolCall.function.name}${statusText}`,
111
- streaming: false,
112
- toolResult: !isError ? toolResult : undefined,
113
- toolCall: editDiffData || terminalResultData
114
- ? {
115
- name: toolCall.function.name,
116
- arguments: toolArgs,
128
+ processedToolCalls.add(toolCall.id);
129
+ }
130
+ continue;
131
+ }
132
+ // Handle regular tool result messages (non-subagent)
133
+ if (msg.role === 'tool' && msg.tool_call_id && !msg.subAgentInternal) {
134
+ const isError = msg.content.startsWith('Error:');
135
+ const statusIcon = isError ? '✗' : '✓';
136
+ const statusText = isError ? `\n └─ ${msg.content}` : '';
137
+ // Find tool name and args from previous assistant message
138
+ let toolName = 'tool';
139
+ let toolArgs = {};
140
+ let editDiffData;
141
+ let terminalResultData;
142
+ for (let j = i - 1; j >= 0; j--) {
143
+ const prevMsg = sessionMessages[j];
144
+ if (!prevMsg)
145
+ continue;
146
+ if (prevMsg.role === 'assistant' &&
147
+ prevMsg.tool_calls &&
148
+ !prevMsg.subAgentInternal) {
149
+ const tc = prevMsg.tool_calls.find(t => t.id === msg.tool_call_id);
150
+ if (tc) {
151
+ toolName = tc.function.name;
152
+ try {
153
+ toolArgs = JSON.parse(tc.function.arguments);
154
+ }
155
+ catch (e) {
156
+ toolArgs = {};
157
+ }
158
+ // Extract edit diff data
159
+ if ((toolName === 'filesystem-edit' ||
160
+ toolName === 'filesystem-edit_search') &&
161
+ !isError) {
162
+ try {
163
+ const resultData = JSON.parse(msg.content);
164
+ if (resultData.oldContent && resultData.newContent) {
165
+ editDiffData = {
166
+ oldContent: resultData.oldContent,
167
+ newContent: resultData.newContent,
168
+ filename: toolArgs.filePath,
169
+ completeOldContent: resultData.completeOldContent,
170
+ completeNewContent: resultData.completeNewContent,
171
+ contextStartLine: resultData.contextStartLine,
172
+ };
173
+ toolArgs.oldContent = resultData.oldContent;
174
+ toolArgs.newContent = resultData.newContent;
175
+ toolArgs.completeOldContent = resultData.completeOldContent;
176
+ toolArgs.completeNewContent = resultData.completeNewContent;
177
+ toolArgs.contextStartLine = resultData.contextStartLine;
178
+ }
179
+ }
180
+ catch (e) {
181
+ // Ignore parse errors
182
+ }
183
+ }
184
+ // Extract terminal result data
185
+ if (toolName === 'terminal-execute' && !isError) {
186
+ try {
187
+ const resultData = JSON.parse(msg.content);
188
+ if (resultData.stdout !== undefined ||
189
+ resultData.stderr !== undefined) {
190
+ terminalResultData = {
191
+ stdout: resultData.stdout,
192
+ stderr: resultData.stderr,
193
+ exitCode: resultData.exitCode,
194
+ command: toolArgs.command,
195
+ };
196
+ }
197
+ }
198
+ catch (e) {
199
+ // Ignore parse errors
117
200
  }
118
- : undefined,
119
- terminalResult: terminalResultData,
120
- });
201
+ }
202
+ break;
203
+ }
121
204
  }
122
205
  }
206
+ uiMessages.push({
207
+ role: 'assistant',
208
+ content: `${statusIcon} ${toolName}${statusText}`,
209
+ streaming: false,
210
+ toolResult: !isError ? msg.content : undefined,
211
+ toolCall: editDiffData || terminalResultData
212
+ ? {
213
+ name: toolName,
214
+ arguments: toolArgs,
215
+ }
216
+ : undefined,
217
+ terminalResult: terminalResultData,
218
+ });
219
+ continue;
123
220
  }
124
- else {
125
- // Add regular message directly
126
- uiMessages.push(uiMessage);
221
+ // Handle regular user and assistant messages
222
+ if (msg.role === 'user' || msg.role === 'assistant') {
223
+ uiMessages.push({
224
+ role: msg.role,
225
+ content: msg.content,
226
+ streaming: false,
227
+ images: msg.images,
228
+ });
229
+ continue;
127
230
  }
128
231
  }
129
232
  return uiMessages;
@@ -0,0 +1,43 @@
1
+ export interface SubAgent {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ tools: string[];
6
+ createdAt: string;
7
+ updatedAt: string;
8
+ }
9
+ export interface SubAgentsConfig {
10
+ agents: SubAgent[];
11
+ }
12
+ /**
13
+ * Get all sub-agents
14
+ */
15
+ export declare function getSubAgents(): SubAgent[];
16
+ /**
17
+ * Get a sub-agent by ID
18
+ */
19
+ export declare function getSubAgent(id: string): SubAgent | null;
20
+ /**
21
+ * Create a new sub-agent
22
+ */
23
+ export declare function createSubAgent(name: string, description: string, tools: string[]): SubAgent;
24
+ /**
25
+ * Update an existing sub-agent
26
+ */
27
+ export declare function updateSubAgent(id: string, updates: {
28
+ name?: string;
29
+ description?: string;
30
+ tools?: string[];
31
+ }): SubAgent | null;
32
+ /**
33
+ * Delete a sub-agent
34
+ */
35
+ export declare function deleteSubAgent(id: string): boolean;
36
+ /**
37
+ * Validate sub-agent data
38
+ */
39
+ export declare function validateSubAgent(data: {
40
+ name: string;
41
+ description: string;
42
+ tools: string[];
43
+ }): string[];
@@ -0,0 +1,126 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ const CONFIG_DIR = join(homedir(), '.snow');
5
+ const SUB_AGENTS_CONFIG_FILE = join(CONFIG_DIR, 'sub-agents.json');
6
+ function ensureConfigDirectory() {
7
+ if (!existsSync(CONFIG_DIR)) {
8
+ mkdirSync(CONFIG_DIR, { recursive: true });
9
+ }
10
+ }
11
+ function generateId() {
12
+ return `agent_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
13
+ }
14
+ /**
15
+ * Get all sub-agents
16
+ */
17
+ export function getSubAgents() {
18
+ try {
19
+ ensureConfigDirectory();
20
+ if (!existsSync(SUB_AGENTS_CONFIG_FILE)) {
21
+ return [];
22
+ }
23
+ const configData = readFileSync(SUB_AGENTS_CONFIG_FILE, 'utf8');
24
+ const config = JSON.parse(configData);
25
+ return config.agents || [];
26
+ }
27
+ catch (error) {
28
+ console.error('Failed to load sub-agents:', error);
29
+ return [];
30
+ }
31
+ }
32
+ /**
33
+ * Get a sub-agent by ID
34
+ */
35
+ export function getSubAgent(id) {
36
+ const agents = getSubAgents();
37
+ return agents.find(agent => agent.id === id) || null;
38
+ }
39
+ /**
40
+ * Save all sub-agents
41
+ */
42
+ function saveSubAgents(agents) {
43
+ try {
44
+ ensureConfigDirectory();
45
+ const config = { agents };
46
+ const configData = JSON.stringify(config, null, 2);
47
+ writeFileSync(SUB_AGENTS_CONFIG_FILE, configData, 'utf8');
48
+ }
49
+ catch (error) {
50
+ throw new Error(`Failed to save sub-agents: ${error}`);
51
+ }
52
+ }
53
+ /**
54
+ * Create a new sub-agent
55
+ */
56
+ export function createSubAgent(name, description, tools) {
57
+ const agents = getSubAgents();
58
+ const now = new Date().toISOString();
59
+ const newAgent = {
60
+ id: generateId(),
61
+ name,
62
+ description,
63
+ tools,
64
+ createdAt: now,
65
+ updatedAt: now,
66
+ };
67
+ agents.push(newAgent);
68
+ saveSubAgents(agents);
69
+ return newAgent;
70
+ }
71
+ /**
72
+ * Update an existing sub-agent
73
+ */
74
+ export function updateSubAgent(id, updates) {
75
+ const agents = getSubAgents();
76
+ const index = agents.findIndex(agent => agent.id === id);
77
+ if (index === -1) {
78
+ return null;
79
+ }
80
+ const existingAgent = agents[index];
81
+ if (!existingAgent) {
82
+ return null;
83
+ }
84
+ const updatedAgent = {
85
+ id: existingAgent.id,
86
+ name: updates.name ?? existingAgent.name,
87
+ description: updates.description ?? existingAgent.description,
88
+ tools: updates.tools ?? existingAgent.tools,
89
+ createdAt: existingAgent.createdAt,
90
+ updatedAt: new Date().toISOString(),
91
+ };
92
+ agents[index] = updatedAgent;
93
+ saveSubAgents(agents);
94
+ return updatedAgent;
95
+ }
96
+ /**
97
+ * Delete a sub-agent
98
+ */
99
+ export function deleteSubAgent(id) {
100
+ const agents = getSubAgents();
101
+ const filteredAgents = agents.filter(agent => agent.id !== id);
102
+ if (filteredAgents.length === agents.length) {
103
+ return false; // Agent not found
104
+ }
105
+ saveSubAgents(filteredAgents);
106
+ return true;
107
+ }
108
+ /**
109
+ * Validate sub-agent data
110
+ */
111
+ export function validateSubAgent(data) {
112
+ const errors = [];
113
+ if (!data.name || data.name.trim().length === 0) {
114
+ errors.push('Agent name is required');
115
+ }
116
+ if (data.name && data.name.length > 100) {
117
+ errors.push('Agent name must be less than 100 characters');
118
+ }
119
+ if (data.description && data.description.length > 500) {
120
+ errors.push('Description must be less than 500 characters');
121
+ }
122
+ if (!data.tools || data.tools.length === 0) {
123
+ errors.push('At least one tool must be selected');
124
+ }
125
+ return errors;
126
+ }
@@ -0,0 +1,29 @@
1
+ export interface SubAgentMessage {
2
+ type: 'sub_agent_message';
3
+ agentId: string;
4
+ agentName: string;
5
+ message: any;
6
+ }
7
+ export interface SubAgentResult {
8
+ success: boolean;
9
+ result: string;
10
+ error?: string;
11
+ }
12
+ export interface ToolConfirmationCallback {
13
+ (toolName: string, toolArgs: any): Promise<string>;
14
+ }
15
+ export interface ToolApprovalChecker {
16
+ (toolName: string): boolean;
17
+ }
18
+ /**
19
+ * Execute a sub-agent as a tool
20
+ * @param agentId - The ID of the sub-agent to execute
21
+ * @param prompt - The task prompt to send to the sub-agent
22
+ * @param onMessage - Callback for streaming sub-agent messages (for UI display)
23
+ * @param abortSignal - Optional abort signal
24
+ * @param requestToolConfirmation - Callback to request tool confirmation from user
25
+ * @param isToolAutoApproved - Function to check if a tool is auto-approved
26
+ * @param yoloMode - Whether YOLO mode is enabled (auto-approve all tools)
27
+ * @returns The final result from the sub-agent
28
+ */
29
+ export declare function executeSubAgent(agentId: string, prompt: string, onMessage?: (message: SubAgentMessage) => void, abortSignal?: AbortSignal, requestToolConfirmation?: ToolConfirmationCallback, isToolAutoApproved?: ToolApprovalChecker, yoloMode?: boolean): Promise<SubAgentResult>;