snow-ai 0.2.16 → 0.2.18
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/models.d.ts +3 -0
- package/dist/api/models.js +101 -17
- package/dist/api/systemPrompt.d.ts +1 -1
- package/dist/api/systemPrompt.js +8 -0
- package/dist/mcp/filesystem.d.ts +2 -0
- package/dist/mcp/filesystem.js +7 -1
- package/dist/ui/components/ChatInput.js +3 -3
- package/dist/ui/components/DiffViewer.d.ts +3 -1
- package/dist/ui/components/DiffViewer.js +105 -76
- package/dist/ui/components/MCPInfoPanel.js +84 -49
- package/dist/ui/components/MCPInfoScreen.js +93 -5
- package/dist/ui/components/ToolConfirmation.js +1 -1
- package/dist/ui/pages/ChatScreen.js +20 -19
- package/dist/utils/mcpToolsManager.d.ts +5 -0
- package/dist/utils/mcpToolsManager.js +64 -0
- package/package.json +1 -1
package/dist/api/models.d.ts
CHANGED
|
@@ -8,5 +8,8 @@ export interface ModelsResponse {
|
|
|
8
8
|
object: string;
|
|
9
9
|
data: Model[];
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Fetch available models based on configured request method
|
|
13
|
+
*/
|
|
11
14
|
export declare function fetchAvailableModels(): Promise<Model[]>;
|
|
12
15
|
export declare function filterModels(models: Model[], searchTerm: string): Model[];
|
package/dist/api/models.js
CHANGED
|
@@ -1,28 +1,112 @@
|
|
|
1
|
-
import { getOpenAiConfig } from '../utils/apiConfig.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { getOpenAiConfig, getCustomHeaders } from '../utils/apiConfig.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fetch models from OpenAI-compatible API
|
|
4
|
+
*/
|
|
5
|
+
async function fetchOpenAIModels(baseUrl, apiKey, customHeaders) {
|
|
6
|
+
const url = `${baseUrl.replace(/\/$/, '')}/models`;
|
|
7
|
+
const headers = {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
...customHeaders,
|
|
10
|
+
};
|
|
11
|
+
if (apiKey) {
|
|
12
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
13
|
+
}
|
|
14
|
+
const response = await fetch(url, {
|
|
15
|
+
method: 'GET',
|
|
16
|
+
headers,
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
|
20
|
+
}
|
|
21
|
+
const data = await response.json();
|
|
22
|
+
return data.data || [];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Fetch models from Gemini API
|
|
26
|
+
*/
|
|
27
|
+
async function fetchGeminiModels(baseUrl, apiKey) {
|
|
28
|
+
// Gemini uses API key as query parameter
|
|
29
|
+
const url = `${baseUrl.replace(/\/$/, '')}/models?key=${apiKey}`;
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
method: 'GET',
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
|
6
38
|
}
|
|
7
|
-
const
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
// Convert Gemini format to standard Model format
|
|
41
|
+
return (data.models || []).map(model => ({
|
|
42
|
+
id: model.name.replace('models/', ''), // Remove "models/" prefix
|
|
43
|
+
object: 'model',
|
|
44
|
+
created: 0,
|
|
45
|
+
owned_by: 'google',
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fetch models from Anthropic API
|
|
50
|
+
*/
|
|
51
|
+
async function fetchAnthropicModels(baseUrl, apiKey, customHeaders) {
|
|
52
|
+
const url = `${baseUrl.replace(/\/$/, '')}/models`;
|
|
8
53
|
const headers = {
|
|
9
54
|
'Content-Type': 'application/json',
|
|
55
|
+
'anthropic-version': '2023-06-01',
|
|
56
|
+
...customHeaders,
|
|
10
57
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
58
|
+
if (apiKey) {
|
|
59
|
+
headers['x-api-key'] = apiKey;
|
|
60
|
+
}
|
|
61
|
+
const response = await fetch(url, {
|
|
62
|
+
method: 'GET',
|
|
63
|
+
headers,
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
|
67
|
+
}
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
// Convert Anthropic format to standard Model format
|
|
70
|
+
return (data.data || []).map(model => ({
|
|
71
|
+
id: model.id,
|
|
72
|
+
object: 'model',
|
|
73
|
+
created: new Date(model.created_at).getTime() / 1000, // Convert to Unix timestamp
|
|
74
|
+
owned_by: 'anthropic',
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Fetch available models based on configured request method
|
|
79
|
+
*/
|
|
80
|
+
export async function fetchAvailableModels() {
|
|
81
|
+
const config = getOpenAiConfig();
|
|
82
|
+
if (!config.baseUrl) {
|
|
83
|
+
throw new Error('Base URL not configured. Please configure API settings first.');
|
|
14
84
|
}
|
|
85
|
+
const customHeaders = getCustomHeaders();
|
|
15
86
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
87
|
+
let models;
|
|
88
|
+
switch (config.requestMethod) {
|
|
89
|
+
case 'gemini':
|
|
90
|
+
if (!config.apiKey) {
|
|
91
|
+
throw new Error('API key is required for Gemini API');
|
|
92
|
+
}
|
|
93
|
+
models = await fetchGeminiModels(config.baseUrl.replace(/\/$/, '') + '/v1beta', config.apiKey);
|
|
94
|
+
break;
|
|
95
|
+
case 'anthropic':
|
|
96
|
+
if (!config.apiKey) {
|
|
97
|
+
throw new Error('API key is required for Anthropic API');
|
|
98
|
+
}
|
|
99
|
+
models = await fetchAnthropicModels(config.baseUrl.replace(/\/$/, '') + '/v1', config.apiKey, customHeaders);
|
|
100
|
+
break;
|
|
101
|
+
case 'chat':
|
|
102
|
+
case 'responses':
|
|
103
|
+
default:
|
|
104
|
+
// OpenAI-compatible API
|
|
105
|
+
models = await fetchOpenAIModels(config.baseUrl, config.apiKey, customHeaders);
|
|
106
|
+
break;
|
|
22
107
|
}
|
|
23
|
-
const data = await response.json();
|
|
24
108
|
// Sort models alphabetically by id for better UX
|
|
25
|
-
return
|
|
109
|
+
return models.sort((a, b) => a.id.localeCompare(b.id));
|
|
26
110
|
}
|
|
27
111
|
catch (error) {
|
|
28
112
|
if (error instanceof Error) {
|
|
@@ -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 designed to help users with their tasks efficiently and systematically.\n\n## \uD83C\uDFAF Core Principles\n\n1. **Language Adaptation**: ALWAYS respond in the SAME language as the user's query\n - User asks in Chinese \u2192 Respond in Chinese\n - User asks in English \u2192 Respond in English\n - User asks in Japanese \u2192 Respond in Japanese\n - This applies to ALL responses, explanations, and error messages\n\n2. **Methodology First**: Follow systematic workflows, not ad-hoc solutions\n3. **Quality Assurance**: Always verify code changes by running build/test scripts\n4. **Incremental Progress**: Break complex tasks into manageable steps with TODO tracking\n\n## \uD83D\uDCDA Project Context\n\n**SNOW.md Documentation:**\n- Check if SNOW.md exists in the project root before making changes\n- SNOW.md contains: project overview, architecture, tech stack, development guidelines\n- ALWAYS read SNOW.md first for complex tasks to understand project context\n- If SNOW.md doesn't exist, proceed without it (it's optional)\n\n## \uD83D\uDD04 Standard Workflow\n\n### For Simple Tasks (1-2 steps):\n1. Understand the request\n2. Execute directly using appropriate tools\n3. Verify the result\n\n### For Complex Tasks (3+ steps):\n1. **Plan**: Create a TODO list with clear, actionable tasks\n2. **Read Context**: Check SNOW.md and relevant files\n3. **Execute**: Work through tasks systematically\n4. **Update**: Mark each task as completed IMMEDIATELY after finishing\n5. **Verify**: Run build/test scripts to catch errors\n6. **Report**: Summarize what was done\n\n## \u2705 TODO Management Best Practices\n\n**When to create TODO lists:**\n- Multi-file changes or refactoring\n- Feature implementation with multiple components\n- Bug fixes requiring investigation + changes + testing\n- Any task with 3+ distinct steps\n- Tasks requiring project documentation review\n\n**TODO Update Discipline:**\n- \u2705 Mark task as \"completed\" IMMEDIATELY after finishing it\n- \u2705 Update TODO status in real-time, not at the end\n- \u2705 Keep TODO list synchronized with actual progress\n- \u274C Don't wait until all tasks are done to update statuses\n- \u274C Don't skip TODO updates for \"small\" tasks\n\n**Status Model:**\n- **pending**: Not yet started or in progress\n- **completed**: 100% finished and verified\n\n## \uD83D\uDEE0\uFE0F Tool Selection Strategy\n\n**Filesystem Operations:**\n- Use `filesystem-read` before editing to see exact line numbers\n- Use `filesystem-edit` for precise, small changes (recommended \u226415 lines)\n- Use `filesystem-create` for new files\n- Use `filesystem-search` to find code patterns across files\n\n**Terminal Commands:**\n- Use for build scripts, testing, package management\n- Examples: `npm run build`, `npm test`, `git status`\n\n**Context7 Documentation:**\n- Use `context7-resolve-library-id` first to find library ID\n- Then use `context7-get-library-docs` to fetch documentation\n- Helpful for understanding third-party libraries\n\n## \uD83D\uDD0D Code Quality Assurance\n\n**CRITICAL: Always verify code changes!**\n\nAfter making code changes, you MUST:\n1. Run the project's build script: `npm run build` or `tsc`\n2. Check for TypeScript/compilation errors\n3. If errors occur, fix them immediately\n4. Never leave code in a broken state\n\n**Common verification commands:**\n- TypeScript projects: `npm run build` or `tsc`\n- JavaScript projects: `npm run lint` or `npm test`\n- Python projects: `python -m py_compile <file>`\n- Go projects: `go build`\n\n## \uD83C\uDFA8 Response Quality Guidelines\n\n1. **Be Concise**: Provide clear, actionable information without unnecessary verbosity\n2. **Use Formatting**: Use markdown, emojis, and structure for readability\n3. **Show Progress**: For complex tasks, show TODO progress and updates\n4. **Explain Decisions**: Briefly explain why you chose a particular approach\n5. **Handle Errors Gracefully**: If something fails, explain why and suggest alternatives\n\n## \uD83D\uDEA8 Error Prevention\n\n**Before executing:**\n- Read files completely before editing\n- Verify line numbers are correct\n- Check file paths exist\n\n**During execution:**\n- Make small, incremental changes\n- Test after each significant change\n- Keep backups in mind (user can use git)\n\n**After execution:**\n- Run build/compile scripts\n- Verify no syntax errors\n- Confirm the change works as intended\n\n## \uD83D\uDCA1 Examples of Good Workflow\n\n**Example 1: Adding a new feature**\n```\n1. Create TODO list with tasks\n2. Read SNOW.md to understand architecture\n3. Read relevant source files\n4. Implement changes incrementally\n5. Update TODO after each file\n6. Run npm run build to verify\n7. Report completion\n```\n\n**Example 2: Fixing a bug**\n```\n1. Search for the bug location\n2. Read surrounding code context\n3. Identify root cause\n4. Make minimal fix\n5. Run build/test scripts\n6. Verify fix works\n```\n\n**Example 3: Refactoring code**\n```\n1. Create TODO with affected files\n2. Read all files to understand dependencies\n3. Refactor one file at a time\n4. Update TODO after each file\n5. Run build after each change\n6. Ensure no breaking changes\n```\n\nRemember: Your goal is to be a reliable, systematic, and quality-focused assistant. Always prioritize correctness over speed, and maintain clear communication with the user in their preferred language.";
|
|
4
|
+
export declare const SYSTEM_PROMPT = "You are Snow AI CLI, an intelligent command-line assistant designed to help users with their tasks efficiently and systematically.\n\n## \uD83C\uDFAF Core Principles\n\n1. **Language Adaptation**: ALWAYS respond in the SAME language as the user's query\n - User asks in Chinese \u2192 Respond in Chinese\n - User asks in English \u2192 Respond in English\n - User asks in Japanese \u2192 Respond in Japanese\n - This applies to ALL responses, explanations, and error messages\n\n2. **Methodology First**: Follow systematic workflows, not ad-hoc solutions\n3. **Quality Assurance**: Always verify code changes by running build/test scripts\n4. **Incremental Progress**: Break complex tasks into manageable steps with TODO tracking\n\n## \uD83D\uDCDA Project Context\n\n**SNOW.md Documentation:**\n- Check if SNOW.md exists in the project root before making changes\n- SNOW.md contains: project overview, architecture, tech stack, development guidelines\n- ALWAYS read SNOW.md first for complex tasks to understand project context\n- If SNOW.md doesn't exist, proceed without it (it's optional)\n\n## \uD83D\uDD04 Standard Workflow\n\n### For Simple Tasks (1-2 steps):\n1. Understand the request\n2. Execute directly using appropriate tools\n3. Verify the result\n\n### For Complex Tasks (3+ steps):\n1. **Plan**: Create a TODO list with clear, actionable tasks\n2. **Read Context**: Check SNOW.md and relevant files\n3. **Execute**: Work through tasks systematically\n4. **Update**: Mark each task as completed IMMEDIATELY after finishing\n5. **Verify**: Run build/test scripts to catch errors\n6. **Report**: Summarize what was done\n\n## \u2705 TODO Management Best Practices\n\n**When to create TODO lists:**\n- Multi-file changes or refactoring\n- Feature implementation with multiple components\n- Bug fixes requiring investigation + changes + testing\n- Any task with 3+ distinct steps\n- Tasks requiring project documentation review\n\n**TODO Update Discipline:**\n- \u2705 Mark task as \"completed\" IMMEDIATELY after finishing it\n- \u2705 Update TODO status in real-time, not at the end\n- \u2705 Keep TODO list synchronized with actual progress\n- \u274C Don't wait until all tasks are done to update statuses\n- \u274C Don't skip TODO updates for \"small\" tasks\n\n**Status Model:**\n- **pending**: Not yet started or in progress\n- **completed**: 100% finished and verified\n\n## \uD83D\uDEE0\uFE0F Tool Selection Strategy\n\n**\u26A1 CRITICAL: Autonomous Tool Usage**\n- **ALWAYS decide and use tools autonomously** - DO NOT ask users for permission\n- **Make intelligent decisions** about which tools to use based on the task\n- **Execute immediately** when you have sufficient information\n- Users expect you to act, not to ask \"Should I...?\" or \"Do you want me to...?\"\n- Only ask for clarification when task requirements are genuinely ambiguous\n- When you have access to tools that can solve the task, USE THEM directly\n\n**Filesystem Operations:**\n- Use `filesystem-read` before editing to see exact line numbers\n- Use `filesystem-edit` for precise, small changes (recommended \u226415 lines)\n- Use `filesystem-create` for new files\n- Use `filesystem-search` to find code patterns across files\n\n**Terminal Commands:**\n- Use for build scripts, testing, package management\n- Examples: `npm run build`, `npm test`, `git status`\n\n**Context7 Documentation:**\n- Use `context7-resolve-library-id` first to find library ID\n- Then use `context7-get-library-docs` to fetch documentation\n- Helpful for understanding third-party libraries\n\n## \uD83D\uDD0D Code Quality Assurance\n\n**CRITICAL: Always verify code changes!**\n\nAfter making code changes, you MUST:\n1. Run the project's build script: `npm run build` or `tsc`\n2. Check for TypeScript/compilation errors\n3. If errors occur, fix them immediately\n4. Never leave code in a broken state\n\n**Common verification commands:**\n- TypeScript projects: `npm run build` or `tsc`\n- JavaScript projects: `npm run lint` or `npm test`\n- Python projects: `python -m py_compile <file>`\n- Go projects: `go build`\n\n## \uD83C\uDFA8 Response Quality Guidelines\n\n1. **Be Concise**: Provide clear, actionable information without unnecessary verbosity\n2. **Use Formatting**: Use markdown, emojis, and structure for readability\n3. **Show Progress**: For complex tasks, show TODO progress and updates\n4. **Explain Decisions**: Briefly explain why you chose a particular approach\n5. **Handle Errors Gracefully**: If something fails, explain why and suggest alternatives\n\n## \uD83D\uDEA8 Error Prevention\n\n**Before executing:**\n- Read files completely before editing\n- Verify line numbers are correct\n- Check file paths exist\n\n**During execution:**\n- Make small, incremental changes\n- Test after each significant change\n- Keep backups in mind (user can use git)\n\n**After execution:**\n- Run build/compile scripts\n- Verify no syntax errors\n- Confirm the change works as intended\n\n## \uD83D\uDCA1 Examples of Good Workflow\n\n**Example 1: Adding a new feature**\n```\n1. Create TODO list with tasks\n2. Read SNOW.md to understand architecture\n3. Read relevant source files\n4. Implement changes incrementally\n5. Update TODO after each file\n6. Run npm run build to verify\n7. Report completion\n```\n\n**Example 2: Fixing a bug**\n```\n1. Search for the bug location\n2. Read surrounding code context\n3. Identify root cause\n4. Make minimal fix\n5. Run build/test scripts\n6. Verify fix works\n```\n\n**Example 3: Refactoring code**\n```\n1. Create TODO with affected files\n2. Read all files to understand dependencies\n3. Refactor one file at a time\n4. Update TODO after each file\n5. Run build after each change\n6. Ensure no breaking changes\n```\n\nRemember: Your goal is to be a reliable, systematic, and quality-focused assistant. Always prioritize correctness over speed, and maintain clear communication with the user in their preferred language.";
|
package/dist/api/systemPrompt.js
CHANGED
|
@@ -60,6 +60,14 @@ export const SYSTEM_PROMPT = `You are Snow AI CLI, an intelligent command-line a
|
|
|
60
60
|
|
|
61
61
|
## 🛠️ Tool Selection Strategy
|
|
62
62
|
|
|
63
|
+
**⚡ CRITICAL: Autonomous Tool Usage**
|
|
64
|
+
- **ALWAYS decide and use tools autonomously** - DO NOT ask users for permission
|
|
65
|
+
- **Make intelligent decisions** about which tools to use based on the task
|
|
66
|
+
- **Execute immediately** when you have sufficient information
|
|
67
|
+
- Users expect you to act, not to ask "Should I...?" or "Do you want me to...?"
|
|
68
|
+
- Only ask for clarification when task requirements are genuinely ambiguous
|
|
69
|
+
- When you have access to tools that can solve the task, USE THEM directly
|
|
70
|
+
|
|
63
71
|
**Filesystem Operations:**
|
|
64
72
|
- Use \`filesystem-read\` before editing to see exact line numbers
|
|
65
73
|
- Use \`filesystem-edit\` for precise, small changes (recommended ≤15 lines)
|
package/dist/mcp/filesystem.d.ts
CHANGED
|
@@ -138,6 +138,8 @@ export declare class FilesystemMCPService {
|
|
|
138
138
|
linesModified: number;
|
|
139
139
|
structureAnalysis?: StructureAnalysis;
|
|
140
140
|
diagnostics?: Diagnostic[];
|
|
141
|
+
completeOldContent?: string;
|
|
142
|
+
completeNewContent?: string;
|
|
141
143
|
}>;
|
|
142
144
|
/**
|
|
143
145
|
* Search for code keywords in files within a directory
|
package/dist/mcp/filesystem.js
CHANGED
|
@@ -536,8 +536,11 @@ export class FilesystemMCPService {
|
|
|
536
536
|
catch (error) {
|
|
537
537
|
// Ignore diagnostics errors, they are optional
|
|
538
538
|
}
|
|
539
|
+
// Prepare complete file contents (without line numbers for diff comparison)
|
|
540
|
+
const completeOldContent = lines.join('\n');
|
|
541
|
+
const completeNewContent = finalLines.join('\n');
|
|
539
542
|
const result = {
|
|
540
|
-
message: `✅ File edited successfully: ${filePath}\n` +
|
|
543
|
+
message: `✅ File edited successfully,Please check the edit results and pay attention to code boundary issues to avoid syntax errors caused by missing closed parts: ${filePath}\n` +
|
|
541
544
|
` Replaced: lines ${startLine}-${adjustedEndLine} (${linesToModify} lines)\n` +
|
|
542
545
|
` Result: ${newContentLines.length} new lines` +
|
|
543
546
|
(smartBoundaries.extended
|
|
@@ -551,6 +554,9 @@ export class FilesystemMCPService {
|
|
|
551
554
|
totalLines: finalTotalLines,
|
|
552
555
|
linesModified: linesToModify,
|
|
553
556
|
structureAnalysis,
|
|
557
|
+
// Add complete file contents for intelligent diff display
|
|
558
|
+
completeOldContent,
|
|
559
|
+
completeNewContent,
|
|
554
560
|
};
|
|
555
561
|
// Add diagnostics if any were found
|
|
556
562
|
if (diagnostics.length > 0) {
|
|
@@ -136,8 +136,8 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
136
136
|
React.createElement(Text, { color: disabled ? 'darkGray' : 'gray', dimColor: true }, disabled ? 'Waiting for response...' : placeholder)));
|
|
137
137
|
}
|
|
138
138
|
}, [buffer, disabled, placeholder]);
|
|
139
|
-
return (React.createElement(Box, { flexDirection: "column",
|
|
140
|
-
showHistoryMenu && (React.createElement(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "#A9C13E", padding: 1 },
|
|
139
|
+
return (React.createElement(Box, { flexDirection: "column", paddingX: 1, width: "100%", key: `input-${showFilePicker ? 'picker' : 'normal'}` },
|
|
140
|
+
showHistoryMenu && (React.createElement(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "#A9C13E", padding: 1, width: "100%" },
|
|
141
141
|
React.createElement(Box, { marginBottom: 1 },
|
|
142
142
|
React.createElement(Text, { color: "cyan" }, "Use \u2191\u2193 keys to navigate, press Enter to select:")),
|
|
143
143
|
React.createElement(Box, { flexDirection: "column" },
|
|
@@ -177,7 +177,7 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
177
177
|
getUserMessages().length - 8,
|
|
178
178
|
" more items")))))),
|
|
179
179
|
!showHistoryMenu && (React.createElement(React.Fragment, null,
|
|
180
|
-
React.createElement(Box, { flexDirection: "row", borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 0, flexGrow: 1 },
|
|
180
|
+
React.createElement(Box, { flexDirection: "row", borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 0, flexGrow: 1, width: "100%" },
|
|
181
181
|
React.createElement(Text, { color: "cyan", bold: true },
|
|
182
182
|
"\u27A3",
|
|
183
183
|
' '),
|
|
@@ -3,6 +3,8 @@ interface Props {
|
|
|
3
3
|
oldContent?: string;
|
|
4
4
|
newContent: string;
|
|
5
5
|
filename?: string;
|
|
6
|
+
completeOldContent?: string;
|
|
7
|
+
completeNewContent?: string;
|
|
6
8
|
}
|
|
7
|
-
export default function DiffViewer({ oldContent, newContent, filename }: Props): React.JSX.Element;
|
|
9
|
+
export default function DiffViewer({ oldContent, newContent, filename, completeOldContent, completeNewContent, }: Props): React.JSX.Element;
|
|
8
10
|
export {};
|
|
@@ -1,120 +1,149 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import * as Diff from 'diff';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
const
|
|
18
|
-
const
|
|
4
|
+
// Helper function to strip line numbers from content (format: "123→content")
|
|
5
|
+
function stripLineNumbers(content) {
|
|
6
|
+
return content
|
|
7
|
+
.split('\n')
|
|
8
|
+
.map(line => {
|
|
9
|
+
// Match pattern: digits + → + content
|
|
10
|
+
const match = line.match(/^\s*\d+→(.*)$/);
|
|
11
|
+
return match ? match[1] : line;
|
|
12
|
+
})
|
|
13
|
+
.join('\n');
|
|
14
|
+
}
|
|
15
|
+
export default function DiffViewer({ oldContent = '', newContent, filename, completeOldContent, completeNewContent, }) {
|
|
16
|
+
// If complete file contents are provided, use them for intelligent diff
|
|
17
|
+
const useCompleteContent = completeOldContent && completeNewContent;
|
|
18
|
+
const diffOldContent = useCompleteContent
|
|
19
|
+
? completeOldContent
|
|
20
|
+
: stripLineNumbers(oldContent);
|
|
21
|
+
const diffNewContent = useCompleteContent
|
|
22
|
+
? completeNewContent
|
|
23
|
+
: stripLineNumbers(newContent);
|
|
19
24
|
// If no old content, show as new file creation
|
|
20
|
-
const isNewFile = !
|
|
25
|
+
const isNewFile = !diffOldContent || diffOldContent.trim() === '';
|
|
21
26
|
if (isNewFile) {
|
|
22
|
-
const allLines =
|
|
23
|
-
const totalLines = allLines.length;
|
|
24
|
-
const lineNumberWidth = String(totalLines).length;
|
|
27
|
+
const allLines = diffNewContent.split('\n');
|
|
25
28
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
26
29
|
React.createElement(Box, { marginBottom: 1 },
|
|
27
30
|
React.createElement(Text, { bold: true, color: "green" }, "[New File]"),
|
|
28
31
|
filename && (React.createElement(Text, { color: "cyan" },
|
|
29
32
|
' ',
|
|
30
33
|
filename))),
|
|
31
|
-
React.createElement(Box, { flexDirection: "column" }, allLines.map((line, index) => (React.createElement(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
" \u2502"),
|
|
35
|
-
React.createElement(Text, { color: "white", backgroundColor: "#006400" },
|
|
36
|
-
"+ ",
|
|
37
|
-
line)))))));
|
|
34
|
+
React.createElement(Box, { flexDirection: "column" }, allLines.map((line, index) => (React.createElement(Text, { key: index, color: "white", backgroundColor: "#006400" },
|
|
35
|
+
"+ ",
|
|
36
|
+
line))))));
|
|
38
37
|
}
|
|
39
|
-
// Generate
|
|
40
|
-
const diffResult = Diff.diffLines(
|
|
41
|
-
|
|
42
|
-
const totalOldLines = cleanOldContent.split('\n').length;
|
|
43
|
-
const totalNewLines = cleanNewContent.split('\n').length;
|
|
44
|
-
const lineNumberWidth = Math.max(String(totalOldLines).length, String(totalNewLines).length, 2);
|
|
45
|
-
const displayLines = [];
|
|
38
|
+
// Generate line-by-line diff
|
|
39
|
+
const diffResult = Diff.diffLines(diffOldContent, diffNewContent);
|
|
40
|
+
const allChanges = [];
|
|
46
41
|
let oldLineNum = 1;
|
|
47
42
|
let newLineNum = 1;
|
|
48
43
|
diffResult.forEach((part) => {
|
|
49
44
|
const lines = part.value.replace(/\n$/, '').split('\n');
|
|
50
45
|
lines.forEach((line) => {
|
|
51
46
|
if (part.added) {
|
|
52
|
-
|
|
47
|
+
allChanges.push({
|
|
53
48
|
type: 'added',
|
|
54
49
|
content: line,
|
|
55
50
|
oldLineNum: null,
|
|
56
|
-
newLineNum: newLineNum
|
|
51
|
+
newLineNum: newLineNum++,
|
|
57
52
|
});
|
|
58
53
|
}
|
|
59
54
|
else if (part.removed) {
|
|
60
|
-
|
|
55
|
+
allChanges.push({
|
|
61
56
|
type: 'removed',
|
|
62
57
|
content: line,
|
|
63
58
|
oldLineNum: oldLineNum++,
|
|
64
|
-
newLineNum: null
|
|
59
|
+
newLineNum: null,
|
|
65
60
|
});
|
|
66
61
|
}
|
|
67
62
|
else {
|
|
68
|
-
|
|
63
|
+
allChanges.push({
|
|
69
64
|
type: 'unchanged',
|
|
70
65
|
content: line,
|
|
71
66
|
oldLineNum: oldLineNum++,
|
|
72
|
-
newLineNum: newLineNum
|
|
67
|
+
newLineNum: newLineNum++,
|
|
73
68
|
});
|
|
74
69
|
}
|
|
75
70
|
});
|
|
76
71
|
});
|
|
72
|
+
// Find diff hunks (groups of changes with context)
|
|
73
|
+
const hunks = [];
|
|
74
|
+
const contextLines = 3; // Number of context lines before and after changes
|
|
75
|
+
for (let i = 0; i < allChanges.length; i++) {
|
|
76
|
+
const change = allChanges[i];
|
|
77
|
+
if (change?.type !== 'unchanged') {
|
|
78
|
+
// Found a change, create a hunk
|
|
79
|
+
const hunkStart = Math.max(0, i - contextLines);
|
|
80
|
+
let hunkEnd = i;
|
|
81
|
+
// Extend the hunk to include all consecutive changes
|
|
82
|
+
while (hunkEnd < allChanges.length - 1) {
|
|
83
|
+
const nextChange = allChanges[hunkEnd + 1];
|
|
84
|
+
if (!nextChange)
|
|
85
|
+
break;
|
|
86
|
+
// If next line is a change, extend the hunk
|
|
87
|
+
if (nextChange.type !== 'unchanged') {
|
|
88
|
+
hunkEnd++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// If there are more changes within context distance, extend the hunk
|
|
92
|
+
let hasMoreChanges = false;
|
|
93
|
+
for (let j = hunkEnd + 1; j < Math.min(allChanges.length, hunkEnd + 1 + contextLines * 2); j++) {
|
|
94
|
+
if (allChanges[j]?.type !== 'unchanged') {
|
|
95
|
+
hasMoreChanges = true;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (hasMoreChanges) {
|
|
100
|
+
hunkEnd++;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Add context lines after the hunk
|
|
107
|
+
hunkEnd = Math.min(allChanges.length - 1, hunkEnd + contextLines);
|
|
108
|
+
// Extract the hunk
|
|
109
|
+
const hunkChanges = allChanges.slice(hunkStart, hunkEnd + 1);
|
|
110
|
+
const firstChange = hunkChanges[0];
|
|
111
|
+
const lastChange = hunkChanges[hunkChanges.length - 1];
|
|
112
|
+
if (firstChange && lastChange) {
|
|
113
|
+
hunks.push({
|
|
114
|
+
startLine: firstChange.oldLineNum || firstChange.newLineNum || 1,
|
|
115
|
+
endLine: lastChange.oldLineNum || lastChange.newLineNum || 1,
|
|
116
|
+
changes: hunkChanges,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Skip to the end of this hunk
|
|
120
|
+
i = hunkEnd;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
77
123
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
78
124
|
React.createElement(Box, { marginBottom: 1 },
|
|
79
125
|
React.createElement(Text, { bold: true, color: "yellow" }, "[File Modified]"),
|
|
80
126
|
filename && (React.createElement(Text, { color: "cyan" },
|
|
81
127
|
' ',
|
|
82
128
|
filename))),
|
|
83
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
React.createElement(Text, { color: "gray", dimColor: true }, oldNum),
|
|
104
|
-
React.createElement(Text, { color: "red", dimColor: true }, ' - '),
|
|
105
|
-
React.createElement(Text, { color: "gray", dimColor: true }, newNum),
|
|
106
|
-
React.createElement(Text, { dimColor: true }, " \u2502 ")),
|
|
107
|
-
React.createElement(Box, null,
|
|
108
|
-
React.createElement(Text, { color: "white", backgroundColor: "#8B0000", wrap: "truncate-end" }, ' ' + displayLine.content))));
|
|
109
|
-
}
|
|
110
|
-
// Unchanged lines
|
|
111
|
-
return (React.createElement(Box, { key: index, flexDirection: "row" },
|
|
112
|
-
React.createElement(Box, { flexShrink: 0 },
|
|
113
|
-
React.createElement(Text, { color: "gray", dimColor: true }, oldNum),
|
|
114
|
-
React.createElement(Text, { dimColor: true }, ' '),
|
|
115
|
-
React.createElement(Text, { color: "gray", dimColor: true }, newNum),
|
|
116
|
-
React.createElement(Text, { dimColor: true }, " \u2502 ")),
|
|
117
|
-
React.createElement(Box, null,
|
|
118
|
-
React.createElement(Text, { dimColor: true, wrap: "truncate-end" }, displayLine.content))));
|
|
119
|
-
}))));
|
|
129
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
130
|
+
hunks.map((hunk, hunkIndex) => (React.createElement(Box, { key: hunkIndex, flexDirection: "column", marginBottom: 1 }, hunk.changes.map((change, changeIndex) => {
|
|
131
|
+
if (change.type === 'added') {
|
|
132
|
+
return (React.createElement(Text, { key: changeIndex, color: "white", backgroundColor: "#006400" },
|
|
133
|
+
"+ ",
|
|
134
|
+
change.content));
|
|
135
|
+
}
|
|
136
|
+
if (change.type === 'removed') {
|
|
137
|
+
return (React.createElement(Text, { key: changeIndex, color: "white", backgroundColor: "#8B0000" },
|
|
138
|
+
"- ",
|
|
139
|
+
change.content));
|
|
140
|
+
}
|
|
141
|
+
// Unchanged lines (context)
|
|
142
|
+
return (React.createElement(Text, { key: changeIndex, dimColor: true }, change.content));
|
|
143
|
+
})))),
|
|
144
|
+
hunks.length > 1 && (React.createElement(Box, { marginTop: 1 },
|
|
145
|
+
React.createElement(Text, { color: "gray", dimColor: true },
|
|
146
|
+
"Total: ",
|
|
147
|
+
hunks.length,
|
|
148
|
+
" change region(s)"))))));
|
|
120
149
|
}
|
|
@@ -1,73 +1,108 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { getMCPServicesInfo, refreshMCPToolsCache, reconnectMCPService } from '../../utils/mcpToolsManager.js';
|
|
4
5
|
export default function MCPInfoPanel() {
|
|
5
6
|
const [mcpStatus, setMcpStatus] = useState([]);
|
|
6
7
|
const [isLoading, setIsLoading] = useState(true);
|
|
7
8
|
const [errorMessage, setErrorMessage] = useState(null);
|
|
9
|
+
const [isReconnecting, setIsReconnecting] = useState(false);
|
|
10
|
+
const loadMCPStatus = async () => {
|
|
11
|
+
try {
|
|
12
|
+
const servicesInfo = await getMCPServicesInfo();
|
|
13
|
+
const statusList = servicesInfo.map(service => ({
|
|
14
|
+
name: service.serviceName,
|
|
15
|
+
connected: service.connected,
|
|
16
|
+
tools: service.tools.map(tool => tool.name),
|
|
17
|
+
connectionMethod: service.isBuiltIn ? 'Built-in' : 'External',
|
|
18
|
+
isBuiltIn: service.isBuiltIn,
|
|
19
|
+
error: service.error
|
|
20
|
+
}));
|
|
21
|
+
setMcpStatus(statusList);
|
|
22
|
+
setErrorMessage(null);
|
|
23
|
+
setIsLoading(false);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
setErrorMessage(error instanceof Error ? error.message : 'Failed to load MCP services');
|
|
27
|
+
setIsLoading(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
8
30
|
useEffect(() => {
|
|
9
31
|
let isMounted = true;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const servicesInfo = await getMCPServicesInfo();
|
|
13
|
-
if (isMounted) {
|
|
14
|
-
const statusList = servicesInfo.map(service => ({
|
|
15
|
-
name: service.serviceName,
|
|
16
|
-
connected: service.connected,
|
|
17
|
-
tools: service.tools.map(tool => tool.name),
|
|
18
|
-
connectionMethod: service.isBuiltIn ? 'Built-in' : 'External',
|
|
19
|
-
isBuiltIn: service.isBuiltIn,
|
|
20
|
-
error: service.error
|
|
21
|
-
}));
|
|
22
|
-
setMcpStatus(statusList);
|
|
23
|
-
setErrorMessage(null);
|
|
24
|
-
setIsLoading(false);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
if (isMounted) {
|
|
29
|
-
setErrorMessage(error instanceof Error ? error.message : 'Failed to load MCP services');
|
|
30
|
-
setIsLoading(false);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
32
|
+
const load = async () => {
|
|
33
|
+
await loadMCPStatus();
|
|
33
34
|
};
|
|
34
|
-
|
|
35
|
+
if (isMounted) {
|
|
36
|
+
load();
|
|
37
|
+
}
|
|
35
38
|
return () => {
|
|
36
39
|
isMounted = false;
|
|
37
40
|
};
|
|
38
41
|
}, []);
|
|
42
|
+
const handleServiceSelect = async (item) => {
|
|
43
|
+
setIsReconnecting(true);
|
|
44
|
+
try {
|
|
45
|
+
if (item.value === 'refresh-all') {
|
|
46
|
+
// Refresh all services
|
|
47
|
+
await refreshMCPToolsCache();
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Reconnect specific service
|
|
51
|
+
await reconnectMCPService(item.value);
|
|
52
|
+
}
|
|
53
|
+
await loadMCPStatus();
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
setErrorMessage(error instanceof Error ? error.message : 'Failed to reconnect');
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
setIsReconnecting(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
// Build select items from all services
|
|
63
|
+
const selectItems = [
|
|
64
|
+
{ label: 'Refresh all services', value: 'refresh-all', isRefreshAll: true },
|
|
65
|
+
...mcpStatus.map(s => ({
|
|
66
|
+
label: s.name,
|
|
67
|
+
value: s.name,
|
|
68
|
+
connected: s.connected,
|
|
69
|
+
isBuiltIn: s.isBuiltIn,
|
|
70
|
+
error: s.error
|
|
71
|
+
}))
|
|
72
|
+
];
|
|
73
|
+
// Custom item component to render with colors
|
|
74
|
+
const ItemComponent = ({ isSelected, label }) => {
|
|
75
|
+
const item = selectItems.find(i => i.label === label);
|
|
76
|
+
if (!item)
|
|
77
|
+
return React.createElement(Text, null, label);
|
|
78
|
+
if (item.isRefreshAll) {
|
|
79
|
+
return (React.createElement(Text, { color: isSelected ? 'cyan' : 'blue' },
|
|
80
|
+
"\u21BB ",
|
|
81
|
+
label));
|
|
82
|
+
}
|
|
83
|
+
const statusColor = item.connected ? 'green' : 'red';
|
|
84
|
+
const suffix = item.isBuiltIn ? ' (System)' : item.connected ? ' (External)' : ` - ${item.error || 'Failed'}`;
|
|
85
|
+
return (React.createElement(Box, null,
|
|
86
|
+
React.createElement(Text, { color: statusColor }, "\u25CF "),
|
|
87
|
+
React.createElement(Text, { color: isSelected ? 'cyan' : 'white' }, label),
|
|
88
|
+
React.createElement(Text, { color: "gray", dimColor: true }, suffix)));
|
|
89
|
+
};
|
|
39
90
|
if (isLoading) {
|
|
40
91
|
return (React.createElement(Text, { color: "gray" }, "Loading MCP services..."));
|
|
41
92
|
}
|
|
42
93
|
if (errorMessage) {
|
|
43
|
-
return (React.createElement(Box, { borderColor: "red", borderStyle: "round", paddingX: 2, paddingY:
|
|
94
|
+
return (React.createElement(Box, { borderColor: "red", borderStyle: "round", paddingX: 2, paddingY: 0 },
|
|
44
95
|
React.createElement(Text, { color: "red", dimColor: true },
|
|
45
|
-
"Error
|
|
96
|
+
"Error: ",
|
|
46
97
|
errorMessage)));
|
|
47
98
|
}
|
|
48
99
|
if (mcpStatus.length === 0) {
|
|
49
|
-
return (React.createElement(Box, { borderColor: "cyan", borderStyle: "round", paddingX: 2, paddingY:
|
|
50
|
-
React.createElement(Text, { color: "gray", dimColor: true }, "No available MCP
|
|
100
|
+
return (React.createElement(Box, { borderColor: "cyan", borderStyle: "round", paddingX: 2, paddingY: 0 },
|
|
101
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "No available MCP services detected")));
|
|
51
102
|
}
|
|
52
|
-
return (React.createElement(Box, { borderColor: "cyan", borderStyle: "round", paddingX: 2, paddingY:
|
|
103
|
+
return (React.createElement(Box, { borderColor: "cyan", borderStyle: "round", paddingX: 2, paddingY: 0 },
|
|
53
104
|
React.createElement(Box, { flexDirection: "column" },
|
|
54
|
-
React.createElement(Text, { color: "cyan", bold: true },
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
React.createElement(Text, { color: status.connected ? "green" : "red" }, status.connected ? "●" : "●"),
|
|
58
|
-
React.createElement(Box, { marginLeft: 1 },
|
|
59
|
-
React.createElement(Text, { color: "white", bold: true }, status.name),
|
|
60
|
-
status.isBuiltIn && (React.createElement(Text, { color: "blue", dimColor: true }, "(System)")),
|
|
61
|
-
status.connected && status.connectionMethod && !status.isBuiltIn && (React.createElement(Text, { color: "gray", dimColor: true },
|
|
62
|
-
"(",
|
|
63
|
-
status.connectionMethod,
|
|
64
|
-
")")))),
|
|
65
|
-
status.connected && status.tools.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 },
|
|
66
|
-
React.createElement(Text, { color: "gray", dimColor: true },
|
|
67
|
-
"Tools: ",
|
|
68
|
-
status.tools.join(', ')))),
|
|
69
|
-
!status.connected && status.error && (React.createElement(Box, { marginLeft: 2 },
|
|
70
|
-
React.createElement(Text, { color: "red", dimColor: true },
|
|
71
|
-
"Error: ",
|
|
72
|
-
status.error)))))))));
|
|
105
|
+
React.createElement(Text, { color: "cyan", bold: true }, isReconnecting ? 'Refreshing services...' : 'MCP Services'),
|
|
106
|
+
!isReconnecting && (React.createElement(SelectInput, { items: selectItems, onSelect: handleServiceSelect, itemComponent: ItemComponent })),
|
|
107
|
+
isReconnecting && (React.createElement(Text, { color: "yellow", dimColor: true }, "Please wait...")))));
|
|
73
108
|
}
|
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
|
-
import
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { getMCPServicesInfo, refreshMCPToolsCache, reconnectMCPService } from '../../utils/mcpToolsManager.js';
|
|
4
5
|
export default function MCPInfoScreen({ onClose, panelKey }) {
|
|
6
|
+
const [mcpStatus, setMcpStatus] = useState([]);
|
|
7
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
8
|
+
const [errorMessage, setErrorMessage] = useState(null);
|
|
9
|
+
const [isReconnecting, setIsReconnecting] = useState(false);
|
|
10
|
+
const loadMCPStatus = async () => {
|
|
11
|
+
try {
|
|
12
|
+
const servicesInfo = await getMCPServicesInfo();
|
|
13
|
+
const statusList = servicesInfo.map(service => ({
|
|
14
|
+
name: service.serviceName,
|
|
15
|
+
connected: service.connected,
|
|
16
|
+
tools: service.tools.map(tool => tool.name),
|
|
17
|
+
connectionMethod: service.isBuiltIn ? 'Built-in' : 'External',
|
|
18
|
+
isBuiltIn: service.isBuiltIn,
|
|
19
|
+
error: service.error
|
|
20
|
+
}));
|
|
21
|
+
setMcpStatus(statusList);
|
|
22
|
+
setErrorMessage(null);
|
|
23
|
+
setIsLoading(false);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
setErrorMessage(error instanceof Error ? error.message : 'Failed to load MCP services');
|
|
27
|
+
setIsLoading(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
5
30
|
useEffect(() => {
|
|
6
31
|
process.stdout.write('\x1B[?1049h');
|
|
7
32
|
process.stdout.write('\x1B[2J');
|
|
@@ -11,17 +36,80 @@ export default function MCPInfoScreen({ onClose, panelKey }) {
|
|
|
11
36
|
process.stdout.write('\x1B[?1049l');
|
|
12
37
|
};
|
|
13
38
|
}, []);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
loadMCPStatus();
|
|
41
|
+
}, [panelKey]);
|
|
14
42
|
useInput((_, key) => {
|
|
15
43
|
if (key.escape) {
|
|
16
44
|
onClose();
|
|
17
45
|
}
|
|
18
46
|
});
|
|
47
|
+
const handleServiceSelect = async (item) => {
|
|
48
|
+
setIsReconnecting(true);
|
|
49
|
+
try {
|
|
50
|
+
if (item.value === 'refresh-all') {
|
|
51
|
+
// Refresh all services
|
|
52
|
+
await refreshMCPToolsCache();
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Reconnect specific service
|
|
56
|
+
await reconnectMCPService(item.value);
|
|
57
|
+
}
|
|
58
|
+
await loadMCPStatus();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
setErrorMessage(error instanceof Error ? error.message : 'Failed to reconnect');
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
setIsReconnecting(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
// Build select items from all services
|
|
68
|
+
const selectItems = [
|
|
69
|
+
{ label: 'Refresh all services', value: 'refresh-all', isRefreshAll: true },
|
|
70
|
+
...mcpStatus.map(s => ({
|
|
71
|
+
label: s.name,
|
|
72
|
+
value: s.name,
|
|
73
|
+
connected: s.connected,
|
|
74
|
+
isBuiltIn: s.isBuiltIn,
|
|
75
|
+
error: s.error
|
|
76
|
+
}))
|
|
77
|
+
];
|
|
78
|
+
// Custom item component to render with colors
|
|
79
|
+
const ItemComponent = ({ isSelected, label }) => {
|
|
80
|
+
const item = selectItems.find(i => i.label === label);
|
|
81
|
+
if (!item)
|
|
82
|
+
return React.createElement(Text, null, label);
|
|
83
|
+
if (item.isRefreshAll) {
|
|
84
|
+
return (React.createElement(Text, { color: isSelected ? 'cyan' : 'blue' },
|
|
85
|
+
"\u21BB ",
|
|
86
|
+
label));
|
|
87
|
+
}
|
|
88
|
+
const statusColor = item.connected ? 'green' : 'red';
|
|
89
|
+
const suffix = item.isBuiltIn ? ' (System)' : item.connected ? ' (External)' : ` - ${item.error || 'Failed'}`;
|
|
90
|
+
return (React.createElement(Box, null,
|
|
91
|
+
React.createElement(Text, { color: statusColor }, "\u25CF "),
|
|
92
|
+
React.createElement(Text, { color: isSelected ? 'cyan' : 'white' }, label),
|
|
93
|
+
React.createElement(Text, { color: "gray", dimColor: true }, suffix)));
|
|
94
|
+
};
|
|
95
|
+
if (isLoading) {
|
|
96
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
97
|
+
React.createElement(Text, { color: "gray" }, "Loading MCP services...")));
|
|
98
|
+
}
|
|
19
99
|
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
20
|
-
React.createElement(Box, {
|
|
100
|
+
React.createElement(Box, { borderStyle: "double", paddingX: 2, paddingY: 0, borderColor: "cyan" },
|
|
21
101
|
React.createElement(Box, { flexDirection: "column" },
|
|
22
102
|
React.createElement(Text, { color: "white", bold: true },
|
|
23
103
|
React.createElement(Text, { color: "cyan" }, "\u2746 "),
|
|
24
104
|
"MCP Services Overview"),
|
|
25
|
-
React.createElement(Text, { color: "gray", dimColor: true }, "Press ESC to return to
|
|
26
|
-
React.createElement(
|
|
105
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "Press ESC to return | Use \u2191\u2193 and Enter to refresh"))),
|
|
106
|
+
errorMessage && (React.createElement(Box, { borderColor: "red", borderStyle: "round", paddingX: 2, paddingY: 0, marginTop: 1 },
|
|
107
|
+
React.createElement(Text, { color: "red", dimColor: true },
|
|
108
|
+
"Error: ",
|
|
109
|
+
errorMessage))),
|
|
110
|
+
React.createElement(Box, { borderColor: "cyan", borderStyle: "round", paddingX: 2, paddingY: 0, marginTop: 1 },
|
|
111
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
112
|
+
React.createElement(Text, { color: "cyan", bold: true }, isReconnecting ? 'Refreshing services...' : 'MCP Services'),
|
|
113
|
+
!isReconnecting && (React.createElement(SelectInput, { items: selectItems, onSelect: handleServiceSelect, itemComponent: ItemComponent })),
|
|
114
|
+
isReconnecting && (React.createElement(Text, { color: "yellow", dimColor: true }, "Please wait..."))))));
|
|
27
115
|
}
|
|
@@ -83,7 +83,7 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
|
|
|
83
83
|
onConfirm(item.value);
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
|
-
return (React.createElement(Box, { flexDirection: "column", marginX: 1, marginY: 1, paddingX: 1 },
|
|
86
|
+
return (React.createElement(Box, { flexDirection: "column", marginX: 1, marginY: 1, borderStyle: 'round', borderColor: 'yellow', paddingX: 1 },
|
|
87
87
|
React.createElement(Box, { marginBottom: 1 },
|
|
88
88
|
React.createElement(Text, { bold: true, color: "yellow" }, "[Tool Confirmation]")),
|
|
89
89
|
!formattedAllTools && (React.createElement(React.Fragment, null,
|
|
@@ -416,19 +416,20 @@ export default function ChatScreen({}) {
|
|
|
416
416
|
React.createElement(Box, { marginTop: 1 },
|
|
417
417
|
React.createElement(Text, { color: "gray", dimColor: true }, "Please resize your terminal window to continue."))));
|
|
418
418
|
}
|
|
419
|
-
return (React.createElement(Box, { flexDirection: "column", height: "100%" },
|
|
419
|
+
return (React.createElement(Box, { flexDirection: "column", height: "100%", width: "100%" },
|
|
420
420
|
React.createElement(Static, { key: remountKey, items: [
|
|
421
|
-
React.createElement(Box, { key: "header",
|
|
422
|
-
React.createElement(Box, {
|
|
423
|
-
React.createElement(
|
|
424
|
-
React.createElement(Text, { color: "
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
421
|
+
React.createElement(Box, { key: "header", paddingX: 1, width: "100%" },
|
|
422
|
+
React.createElement(Box, { borderColor: 'cyan', borderStyle: "round", paddingX: 2, paddingY: 1, width: "100%" },
|
|
423
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
424
|
+
React.createElement(Text, { color: "white", bold: true },
|
|
425
|
+
React.createElement(Text, { color: "cyan" }, "\u2746 "),
|
|
426
|
+
React.createElement(Gradient, { name: "rainbow" }, "Programming efficiency x10!"),
|
|
427
|
+
React.createElement(Text, { color: "white" }, " \u26C7")),
|
|
428
|
+
React.createElement(Text, null, "\u2022 Ask for code explanations and debugging help"),
|
|
429
|
+
React.createElement(Text, null, "\u2022 Press ESC during response to interrupt"),
|
|
430
|
+
React.createElement(Text, null,
|
|
431
|
+
"\u2022 Working directory: ",
|
|
432
|
+
workingDirectory)))),
|
|
432
433
|
...messages
|
|
433
434
|
.filter(m => !m.streaming && !m.toolPending)
|
|
434
435
|
.map((message, index) => {
|
|
@@ -452,7 +453,7 @@ export default function ChatScreen({}) {
|
|
|
452
453
|
toolStatusColor = 'blue';
|
|
453
454
|
}
|
|
454
455
|
}
|
|
455
|
-
return (React.createElement(Box, { key: `msg-${index}`, marginBottom: isToolMessage ? 0 : 1,
|
|
456
|
+
return (React.createElement(Box, { key: `msg-${index}`, marginBottom: isToolMessage ? 0 : 1, paddingX: 1, flexDirection: "column", width: "100%" },
|
|
456
457
|
React.createElement(Box, null,
|
|
457
458
|
React.createElement(Text, { color: message.role === 'user'
|
|
458
459
|
? 'green'
|
|
@@ -492,7 +493,7 @@ export default function ChatScreen({}) {
|
|
|
492
493
|
message.toolCall.name === 'filesystem-edit' &&
|
|
493
494
|
message.toolCall.arguments.oldContent &&
|
|
494
495
|
message.toolCall.arguments.newContent && (React.createElement(Box, { marginTop: 1 },
|
|
495
|
-
React.createElement(DiffViewer, { oldContent: message.toolCall.arguments.oldContent, newContent: message.toolCall.arguments.newContent, filename: message.toolCall.arguments.filename }))),
|
|
496
|
+
React.createElement(DiffViewer, { oldContent: message.toolCall.arguments.oldContent, newContent: message.toolCall.arguments.newContent, filename: message.toolCall.arguments.filename, completeOldContent: message.toolCall.arguments.completeOldContent, completeNewContent: message.toolCall.arguments.completeNewContent }))),
|
|
496
497
|
message.toolCall &&
|
|
497
498
|
message.toolCall.name === 'terminal-execute' &&
|
|
498
499
|
message.toolCall.arguments.command && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
@@ -565,14 +566,14 @@ export default function ChatScreen({}) {
|
|
|
565
566
|
] }, item => item),
|
|
566
567
|
messages
|
|
567
568
|
.filter(m => m.toolPending)
|
|
568
|
-
.map((message, index) => (React.createElement(Box, { key: `pending-tool-${index}`, marginBottom: 1,
|
|
569
|
+
.map((message, index) => (React.createElement(Box, { key: `pending-tool-${index}`, marginBottom: 1, paddingX: 1, width: "100%" },
|
|
569
570
|
React.createElement(Text, { color: "yellowBright", bold: true }, "\u2746"),
|
|
570
571
|
React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "row" },
|
|
571
572
|
React.createElement(MarkdownRenderer, { content: message.content || ' ', color: "yellow" }),
|
|
572
573
|
React.createElement(Box, { marginLeft: 1 },
|
|
573
574
|
React.createElement(Text, { color: "yellow" },
|
|
574
575
|
React.createElement(Spinner, { type: "dots" }))))))),
|
|
575
|
-
(streamingState.isStreaming || isSaving) && !pendingToolConfirmation && (React.createElement(Box, { marginBottom: 1,
|
|
576
|
+
(streamingState.isStreaming || isSaving) && !pendingToolConfirmation && (React.createElement(Box, { marginBottom: 1, paddingX: 1, width: "100%" },
|
|
576
577
|
React.createElement(Text, { color: ['#FF6EBF', 'green', 'blue', 'cyan', '#B588F8'][streamingState.animationFrame], bold: true }, "\u2746"),
|
|
577
578
|
React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "column" }, streamingState.isStreaming ? (React.createElement(React.Fragment, null, streamingState.retryStatus && streamingState.retryStatus.isRetrying ? (
|
|
578
579
|
// Retry status display - hide "Thinking" and show retry info
|
|
@@ -605,15 +606,15 @@ export default function ChatScreen({}) {
|
|
|
605
606
|
' ',
|
|
606
607
|
"tokens"),
|
|
607
608
|
")")))) : (React.createElement(Text, { color: "gray", dimColor: true }, "Create the first dialogue record file..."))))),
|
|
608
|
-
React.createElement(Box, {
|
|
609
|
+
React.createElement(Box, { paddingX: 1, width: "100%" },
|
|
609
610
|
React.createElement(PendingMessages, { pendingMessages: pendingMessages })),
|
|
610
611
|
pendingToolConfirmation && (React.createElement(ToolConfirmation, { toolName: pendingToolConfirmation.batchToolNames ||
|
|
611
612
|
pendingToolConfirmation.tool.function.name, toolArguments: !pendingToolConfirmation.allTools
|
|
612
613
|
? pendingToolConfirmation.tool.function.arguments
|
|
613
614
|
: undefined, allTools: pendingToolConfirmation.allTools, onConfirm: pendingToolConfirmation.resolve })),
|
|
614
|
-
showSessionPanel && (React.createElement(Box, {
|
|
615
|
+
showSessionPanel && (React.createElement(Box, { paddingX: 1, width: "100%" },
|
|
615
616
|
React.createElement(SessionListPanel, { onSelectSession: handleSessionPanelSelect, onClose: () => setShowSessionPanel(false) }))),
|
|
616
|
-
showMcpPanel && (React.createElement(Box, {
|
|
617
|
+
showMcpPanel && (React.createElement(Box, { paddingX: 1, flexDirection: "column", width: "100%" },
|
|
617
618
|
React.createElement(MCPInfoPanel, null),
|
|
618
619
|
React.createElement(Box, { marginTop: 1 },
|
|
619
620
|
React.createElement(Text, { color: "gray", dimColor: true }, "Press ESC to close")))),
|
|
@@ -26,6 +26,11 @@ export declare function getTodoService(): TodoService;
|
|
|
26
26
|
* Manually refresh the tools cache (for configuration changes)
|
|
27
27
|
*/
|
|
28
28
|
export declare function refreshMCPToolsCache(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Reconnect a specific MCP service and update cache
|
|
31
|
+
* @param serviceName - Name of the service to reconnect
|
|
32
|
+
*/
|
|
33
|
+
export declare function reconnectMCPService(serviceName: string): Promise<void>;
|
|
29
34
|
/**
|
|
30
35
|
* Clear the tools cache (useful for testing or forcing refresh)
|
|
31
36
|
*/
|
|
@@ -182,6 +182,70 @@ export async function refreshMCPToolsCache() {
|
|
|
182
182
|
toolsCache = null;
|
|
183
183
|
await refreshToolsCache();
|
|
184
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Reconnect a specific MCP service and update cache
|
|
187
|
+
* @param serviceName - Name of the service to reconnect
|
|
188
|
+
*/
|
|
189
|
+
export async function reconnectMCPService(serviceName) {
|
|
190
|
+
if (!toolsCache) {
|
|
191
|
+
// If no cache, do full refresh
|
|
192
|
+
await refreshToolsCache();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// Handle built-in services (they don't need reconnection)
|
|
196
|
+
if (serviceName === 'filesystem' || serviceName === 'terminal' || serviceName === 'todo') {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// Get the server config
|
|
200
|
+
const mcpConfig = getMCPConfig();
|
|
201
|
+
const server = mcpConfig.mcpServers[serviceName];
|
|
202
|
+
if (!server) {
|
|
203
|
+
throw new Error(`Service ${serviceName} not found in configuration`);
|
|
204
|
+
}
|
|
205
|
+
// Find and update the service in cache
|
|
206
|
+
const serviceIndex = toolsCache.servicesInfo.findIndex(s => s.serviceName === serviceName);
|
|
207
|
+
if (serviceIndex === -1) {
|
|
208
|
+
// Service not in cache, do full refresh
|
|
209
|
+
await refreshToolsCache();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
// Try to reconnect to the service
|
|
214
|
+
const serviceTools = await probeServiceTools(serviceName, server);
|
|
215
|
+
// Update service info in cache
|
|
216
|
+
toolsCache.servicesInfo[serviceIndex] = {
|
|
217
|
+
serviceName,
|
|
218
|
+
tools: serviceTools,
|
|
219
|
+
isBuiltIn: false,
|
|
220
|
+
connected: true,
|
|
221
|
+
};
|
|
222
|
+
// Remove old tools for this service from the tools list
|
|
223
|
+
toolsCache.tools = toolsCache.tools.filter(tool => !tool.function.name.startsWith(`${serviceName}-`));
|
|
224
|
+
// Add new tools for this service
|
|
225
|
+
for (const tool of serviceTools) {
|
|
226
|
+
toolsCache.tools.push({
|
|
227
|
+
type: 'function',
|
|
228
|
+
function: {
|
|
229
|
+
name: `${serviceName}-${tool.name}`,
|
|
230
|
+
description: tool.description,
|
|
231
|
+
parameters: tool.inputSchema,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
// Update service as failed
|
|
238
|
+
toolsCache.servicesInfo[serviceIndex] = {
|
|
239
|
+
serviceName,
|
|
240
|
+
tools: [],
|
|
241
|
+
isBuiltIn: false,
|
|
242
|
+
connected: false,
|
|
243
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
244
|
+
};
|
|
245
|
+
// Remove tools for this service from the tools list
|
|
246
|
+
toolsCache.tools = toolsCache.tools.filter(tool => !tool.function.name.startsWith(`${serviceName}-`));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
185
249
|
/**
|
|
186
250
|
* Clear the tools cache (useful for testing or forcing refresh)
|
|
187
251
|
*/
|