acmecode 1.0.0
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/.acmecode/config.json +6 -0
- package/README.md +124 -0
- package/dist/agent/index.js +161 -0
- package/dist/cli/bin/acmecode.js +3 -0
- package/dist/cli/package.json +25 -0
- package/dist/cli/src/index.d.ts +1 -0
- package/dist/cli/src/index.js +53 -0
- package/dist/config/index.js +92 -0
- package/dist/context/index.js +30 -0
- package/dist/core/src/agent/index.d.ts +52 -0
- package/dist/core/src/agent/index.js +476 -0
- package/dist/core/src/config/index.d.ts +83 -0
- package/dist/core/src/config/index.js +318 -0
- package/dist/core/src/context/index.d.ts +1 -0
- package/dist/core/src/context/index.js +30 -0
- package/dist/core/src/llm/provider.d.ts +27 -0
- package/dist/core/src/llm/provider.js +202 -0
- package/dist/core/src/llm/vision.d.ts +7 -0
- package/dist/core/src/llm/vision.js +37 -0
- package/dist/core/src/mcp/index.d.ts +10 -0
- package/dist/core/src/mcp/index.js +84 -0
- package/dist/core/src/prompt/anthropic.d.ts +1 -0
- package/dist/core/src/prompt/anthropic.js +32 -0
- package/dist/core/src/prompt/architect.d.ts +1 -0
- package/dist/core/src/prompt/architect.js +17 -0
- package/dist/core/src/prompt/autopilot.d.ts +1 -0
- package/dist/core/src/prompt/autopilot.js +18 -0
- package/dist/core/src/prompt/beast.d.ts +1 -0
- package/dist/core/src/prompt/beast.js +83 -0
- package/dist/core/src/prompt/gemini.d.ts +1 -0
- package/dist/core/src/prompt/gemini.js +45 -0
- package/dist/core/src/prompt/index.d.ts +18 -0
- package/dist/core/src/prompt/index.js +239 -0
- package/dist/core/src/prompt/zen.d.ts +1 -0
- package/dist/core/src/prompt/zen.js +13 -0
- package/dist/core/src/session/index.d.ts +18 -0
- package/dist/core/src/session/index.js +97 -0
- package/dist/core/src/skills/index.d.ts +6 -0
- package/dist/core/src/skills/index.js +72 -0
- package/dist/core/src/tools/batch.d.ts +2 -0
- package/dist/core/src/tools/batch.js +65 -0
- package/dist/core/src/tools/browser.d.ts +7 -0
- package/dist/core/src/tools/browser.js +86 -0
- package/dist/core/src/tools/edit.d.ts +11 -0
- package/dist/core/src/tools/edit.js +312 -0
- package/dist/core/src/tools/index.d.ts +13 -0
- package/dist/core/src/tools/index.js +980 -0
- package/dist/core/src/tools/lsp-client.d.ts +11 -0
- package/dist/core/src/tools/lsp-client.js +224 -0
- package/dist/index.js +41 -0
- package/dist/llm/provider.js +34 -0
- package/dist/mcp/index.js +84 -0
- package/dist/session/index.js +74 -0
- package/dist/skills/index.js +32 -0
- package/dist/tools/index.js +96 -0
- package/dist/tui/App.js +297 -0
- package/dist/tui/Spinner.js +16 -0
- package/dist/tui/TextInput.js +98 -0
- package/dist/tui/src/App.d.ts +11 -0
- package/dist/tui/src/App.js +1211 -0
- package/dist/tui/src/CatLogo.d.ts +10 -0
- package/dist/tui/src/CatLogo.js +99 -0
- package/dist/tui/src/OptionList.d.ts +15 -0
- package/dist/tui/src/OptionList.js +60 -0
- package/dist/tui/src/Spinner.d.ts +7 -0
- package/dist/tui/src/Spinner.js +18 -0
- package/dist/tui/src/TextInput.d.ts +28 -0
- package/dist/tui/src/TextInput.js +139 -0
- package/dist/tui/src/Tips.d.ts +2 -0
- package/dist/tui/src/Tips.js +62 -0
- package/dist/tui/src/Toast.d.ts +19 -0
- package/dist/tui/src/Toast.js +39 -0
- package/dist/tui/src/TodoItem.d.ts +7 -0
- package/dist/tui/src/TodoItem.js +21 -0
- package/dist/tui/src/i18n.d.ts +172 -0
- package/dist/tui/src/i18n.js +189 -0
- package/dist/tui/src/markdown.d.ts +6 -0
- package/dist/tui/src/markdown.js +356 -0
- package/dist/tui/src/theme.d.ts +31 -0
- package/dist/tui/src/theme.js +239 -0
- package/output.txt +0 -0
- package/package.json +44 -0
- package/packages/cli/package.json +25 -0
- package/packages/cli/src/index.ts +59 -0
- package/packages/cli/tsconfig.json +26 -0
- package/packages/core/package.json +39 -0
- package/packages/core/src/agent/index.ts +588 -0
- package/packages/core/src/config/index.ts +383 -0
- package/packages/core/src/context/index.ts +34 -0
- package/packages/core/src/llm/provider.ts +237 -0
- package/packages/core/src/llm/vision.ts +43 -0
- package/packages/core/src/mcp/index.ts +110 -0
- package/packages/core/src/prompt/anthropic.ts +32 -0
- package/packages/core/src/prompt/architect.ts +17 -0
- package/packages/core/src/prompt/autopilot.ts +18 -0
- package/packages/core/src/prompt/beast.ts +83 -0
- package/packages/core/src/prompt/gemini.ts +45 -0
- package/packages/core/src/prompt/index.ts +267 -0
- package/packages/core/src/prompt/zen.ts +13 -0
- package/packages/core/src/session/index.ts +129 -0
- package/packages/core/src/skills/index.ts +86 -0
- package/packages/core/src/tools/batch.ts +73 -0
- package/packages/core/src/tools/browser.ts +95 -0
- package/packages/core/src/tools/edit.ts +317 -0
- package/packages/core/src/tools/index.ts +1112 -0
- package/packages/core/src/tools/lsp-client.ts +303 -0
- package/packages/core/tsconfig.json +19 -0
- package/packages/tui/package.json +24 -0
- package/packages/tui/src/App.tsx +1702 -0
- package/packages/tui/src/CatLogo.tsx +134 -0
- package/packages/tui/src/OptionList.tsx +95 -0
- package/packages/tui/src/Spinner.tsx +28 -0
- package/packages/tui/src/TextInput.tsx +202 -0
- package/packages/tui/src/Tips.tsx +64 -0
- package/packages/tui/src/Toast.tsx +60 -0
- package/packages/tui/src/TodoItem.tsx +29 -0
- package/packages/tui/src/i18n.ts +203 -0
- package/packages/tui/src/markdown.ts +403 -0
- package/packages/tui/src/theme.ts +287 -0
- package/packages/tui/tsconfig.json +24 -0
- package/tsconfig.json +18 -0
- package/vscode-acmecode/.vscodeignore +11 -0
- package/vscode-acmecode/README.md +57 -0
- package/vscode-acmecode/esbuild.js +46 -0
- package/vscode-acmecode/images/button-dark.svg +5 -0
- package/vscode-acmecode/images/button-light.svg +5 -0
- package/vscode-acmecode/images/icon.png +1 -0
- package/vscode-acmecode/package-lock.json +490 -0
- package/vscode-acmecode/package.json +87 -0
- package/vscode-acmecode/src/extension.ts +128 -0
- package/vscode-acmecode/tsconfig.json +16 -0
package/dist/tui/App.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
import { Box, Text, Static } from 'ink';
|
|
3
|
+
import TextInput from './TextInput.js';
|
|
4
|
+
import Spinner from './Spinner.js';
|
|
5
|
+
import { runAgent } from '../agent/index.js';
|
|
6
|
+
import { loadSession, saveMessages } from '../session/index.js';
|
|
7
|
+
import { loadSkills } from '../skills/index.js';
|
|
8
|
+
import { getProjectContext } from '../context/index.js';
|
|
9
|
+
import { getProviderKey, getProviderBaseUrl, loadModelConfig, saveProjectModelConfig } from '../config/index.js';
|
|
10
|
+
let cachedModels = [];
|
|
11
|
+
let itemCounter = 0;
|
|
12
|
+
function extractContent(msg) {
|
|
13
|
+
if (typeof msg.content === 'string')
|
|
14
|
+
return msg.content;
|
|
15
|
+
if (Array.isArray(msg.content)) {
|
|
16
|
+
return msg.content.map((c) => {
|
|
17
|
+
if (c.type === 'text')
|
|
18
|
+
return c.text;
|
|
19
|
+
if (c.type === 'tool-call')
|
|
20
|
+
return `\n> [Tool Call] ${c.toolName}...\n`;
|
|
21
|
+
if (c.type === 'tool-result') {
|
|
22
|
+
const r = typeof c.result === 'string' ? c.result : JSON.stringify(c.result, null, 2);
|
|
23
|
+
return `\n> [Result: ${c.toolName}]\n${r}\n`;
|
|
24
|
+
}
|
|
25
|
+
return '';
|
|
26
|
+
}).join('');
|
|
27
|
+
}
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
export function App({ sessionId, onExit }) {
|
|
31
|
+
// displayItems: permanently rendered messages (Static)
|
|
32
|
+
const [displayItems, setDisplayItems] = useState(() => {
|
|
33
|
+
try {
|
|
34
|
+
const saved = loadSession(sessionId) || [];
|
|
35
|
+
return saved
|
|
36
|
+
.filter((m) => m.role !== 'tool')
|
|
37
|
+
.map((m) => {
|
|
38
|
+
const text = extractContent(m);
|
|
39
|
+
if (!text && m.role === 'assistant')
|
|
40
|
+
return null;
|
|
41
|
+
return { id: String(itemCounter++), role: m.role, text };
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const [currentInput, setCurrentInput] = useState('');
|
|
50
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
51
|
+
const [streamingText, setStreamingText] = useState('');
|
|
52
|
+
const [statusText, setStatusText] = useState('');
|
|
53
|
+
const [activeSkill, setActiveSkill] = useState(null);
|
|
54
|
+
const [provider, setProvider] = useState(() => {
|
|
55
|
+
const cfg = loadModelConfig();
|
|
56
|
+
return cfg.provider;
|
|
57
|
+
});
|
|
58
|
+
const [modelName, setModelName] = useState(() => {
|
|
59
|
+
const cfg = loadModelConfig();
|
|
60
|
+
return cfg.model;
|
|
61
|
+
});
|
|
62
|
+
// Keep a ref for the full messages array (for LLM context), avoid re-rendering on changes
|
|
63
|
+
const messagesRef = useRef((() => {
|
|
64
|
+
try {
|
|
65
|
+
return loadSession(sessionId) || [];
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
})());
|
|
71
|
+
const addDisplayItem = (role, text) => {
|
|
72
|
+
const item = { id: String(itemCounter++), role, text };
|
|
73
|
+
setDisplayItems(prev => [...prev, item]);
|
|
74
|
+
return item;
|
|
75
|
+
};
|
|
76
|
+
const handleSubmit = async (value) => {
|
|
77
|
+
if (value.trim() === '/exit' || value.trim() === '/quit') {
|
|
78
|
+
onExit();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (value.trim() === '/clear') {
|
|
82
|
+
setDisplayItems([]);
|
|
83
|
+
setCurrentInput('');
|
|
84
|
+
setActiveSkill(null);
|
|
85
|
+
messagesRef.current = [];
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (value.startsWith('/skill')) {
|
|
89
|
+
const skillName = value.split(' ')[1];
|
|
90
|
+
setCurrentInput('');
|
|
91
|
+
if (!skillName) {
|
|
92
|
+
const skills = await loadSkills();
|
|
93
|
+
addDisplayItem('assistant', `Available skills:\n${skills.length ? skills.map(s => `- ${s.name}: ${s.description}`).join('\n') : 'No skills found'}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const skills = await loadSkills();
|
|
97
|
+
const skill = skills.find(s => s.name === skillName);
|
|
98
|
+
if (skill) {
|
|
99
|
+
setActiveSkill(skill);
|
|
100
|
+
addDisplayItem('assistant', `Activated skill: ${skill.name}`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
addDisplayItem('assistant', `Skill not found: ${skillName}`);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (value.startsWith('/model')) {
|
|
108
|
+
const args = value.split(' ').slice(1).join(' ').trim();
|
|
109
|
+
setCurrentInput('');
|
|
110
|
+
if (args && /^\d+$/.test(args)) {
|
|
111
|
+
const idx = parseInt(args, 10) - 1;
|
|
112
|
+
if (cachedModels.length > 0 && idx >= 0 && idx < cachedModels.length) {
|
|
113
|
+
const selected = cachedModels[idx];
|
|
114
|
+
setProvider('openai');
|
|
115
|
+
setModelName(selected.id);
|
|
116
|
+
saveProjectModelConfig('openai', selected.id);
|
|
117
|
+
addDisplayItem('assistant', `✅ Switched to: ${selected.id} (saved to .acmecode/config.json)`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
addDisplayItem('assistant', `Invalid number. Use /model to list available models first.`);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (args && args.includes(':')) {
|
|
125
|
+
const parts = args.split(':');
|
|
126
|
+
const newProvider = parts[0];
|
|
127
|
+
const newModel = parts.slice(1).join(':');
|
|
128
|
+
if (['openai', 'anthropic', 'google'].includes(newProvider)) {
|
|
129
|
+
setProvider(newProvider);
|
|
130
|
+
setModelName(newModel);
|
|
131
|
+
saveProjectModelConfig(newProvider, newModel);
|
|
132
|
+
addDisplayItem('assistant', `✅ Switched to ${newProvider}:${newModel} (saved to .acmecode/config.json)`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
addDisplayItem('assistant', `Unknown provider: ${newProvider}. Supported: openai, anthropic, google`);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
addDisplayItem('assistant', `Current model: ${provider}:${modelName}\nFetching available models...`);
|
|
140
|
+
try {
|
|
141
|
+
const apiKey = getProviderKey('openai');
|
|
142
|
+
const baseUrl = getProviderBaseUrl('openai');
|
|
143
|
+
if (!apiKey || !baseUrl) {
|
|
144
|
+
addDisplayItem('assistant', 'Error: API key or Base URL not configured.');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const res = await fetch(`${baseUrl}/models`, {
|
|
148
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
149
|
+
});
|
|
150
|
+
if (!res.ok)
|
|
151
|
+
throw new Error(`HTTP ${res.status}`);
|
|
152
|
+
const data = await res.json();
|
|
153
|
+
const models = (data.data || []);
|
|
154
|
+
cachedModels = models;
|
|
155
|
+
if (models.length === 0) {
|
|
156
|
+
addDisplayItem('assistant', 'No models found.');
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const list = models.map((m, i) => ` ${i + 1}. ${m.id}`).join('\n');
|
|
160
|
+
addDisplayItem('assistant', `📋 Available models:\n${list}\n\nUse /model <number> to select.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
addDisplayItem('assistant', `Error: ${err.message}`);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (value.trim() && !isGenerating) {
|
|
169
|
+
// Show user message immediately (goes into Static)
|
|
170
|
+
addDisplayItem('user', value);
|
|
171
|
+
setCurrentInput('');
|
|
172
|
+
setIsGenerating(true);
|
|
173
|
+
setStreamingText('');
|
|
174
|
+
const currentMessages = Array.isArray(messagesRef.current) ? messagesRef.current : [];
|
|
175
|
+
const newMessages = [...currentMessages, { role: 'user', content: value }];
|
|
176
|
+
try {
|
|
177
|
+
let systemPrompt = `You are AcmeCode, an AI coding assistant. Help the user with their programming tasks. You have access to tools that can read/write files and run shell commands. Always respond in the same language as the user.
|
|
178
|
+
|
|
179
|
+
Current working directory: ${process.cwd()}
|
|
180
|
+
When creating or reading files, always use relative paths (e.g. "snake.html", "src/app.ts") — they will be resolved against the current working directory automatically. Do NOT use absolute paths.
|
|
181
|
+
|
|
182
|
+
CRITICAL INSTRUCTIONS FOR TOOL CALLING:
|
|
183
|
+
1. You MUST use the official JSON schema format when calling tools.
|
|
184
|
+
2. NEVER output fake tool calls in plain text or markdown (e.g., do NOT write \`[read_file(...)]\` or \`call_tool(...)\` in your response).
|
|
185
|
+
3. After using any tool, you MUST provide a brief text summary of what you did and the result. Never end your turn with just a tool call — always follow up with a human-readable explanation.`;
|
|
186
|
+
const projectContext = await getProjectContext();
|
|
187
|
+
if (projectContext)
|
|
188
|
+
systemPrompt += projectContext;
|
|
189
|
+
if (activeSkill)
|
|
190
|
+
systemPrompt += `\n\nActive Skill Instructions:\n${activeSkill.content}`;
|
|
191
|
+
const generator = runAgent(provider, modelName, newMessages, systemPrompt);
|
|
192
|
+
let fullResponse = '';
|
|
193
|
+
let lastFlush = Date.now();
|
|
194
|
+
let didGetMessages = false;
|
|
195
|
+
setStatusText('思考中...');
|
|
196
|
+
// Idle timer: if no events for 2s, show "generating code" status
|
|
197
|
+
let idleTimer = null;
|
|
198
|
+
const resetIdle = () => {
|
|
199
|
+
if (idleTimer)
|
|
200
|
+
clearTimeout(idleTimer);
|
|
201
|
+
idleTimer = setTimeout(() => {
|
|
202
|
+
setStreamingText(fullResponse); // flush any throttled text
|
|
203
|
+
setStatusText('⏳ 生成代码中,请稍候...');
|
|
204
|
+
}, 2000);
|
|
205
|
+
};
|
|
206
|
+
resetIdle();
|
|
207
|
+
for await (const event of generator) {
|
|
208
|
+
resetIdle();
|
|
209
|
+
if (event.type === 'text') {
|
|
210
|
+
setStatusText('');
|
|
211
|
+
fullResponse += event.text;
|
|
212
|
+
const now = Date.now();
|
|
213
|
+
if (now - lastFlush > 100) {
|
|
214
|
+
setStreamingText(fullResponse);
|
|
215
|
+
lastFlush = now;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (event.type === 'tool-call') {
|
|
219
|
+
const toolIcons = {
|
|
220
|
+
read_file: '📖 读取文件',
|
|
221
|
+
write_file: '📝 写入文件',
|
|
222
|
+
run_command: '⚡ 执行命令',
|
|
223
|
+
list_dir: '📂 列出目录',
|
|
224
|
+
};
|
|
225
|
+
const icon = toolIcons[event.name] || `🔧 ${event.name}`;
|
|
226
|
+
// Try to extract a meaningful detail from args
|
|
227
|
+
let detail = '';
|
|
228
|
+
const args = event.args || {};
|
|
229
|
+
if (args.path) {
|
|
230
|
+
detail = `: ${args.path}`;
|
|
231
|
+
}
|
|
232
|
+
else if (args.command) {
|
|
233
|
+
detail = `: ${args.command}`;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Show all arg keys as fallback (skip large values like 'content')
|
|
237
|
+
const keys = Object.keys(args).filter(k => k !== 'content');
|
|
238
|
+
if (keys.length > 0) {
|
|
239
|
+
detail = `: ${keys.map(k => `${k}=${JSON.stringify(args[k]).slice(0, 50)}`).join(', ')}`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
setStatusText(`${icon}${detail}`);
|
|
243
|
+
fullResponse += `\n> ${icon}${detail}\n`;
|
|
244
|
+
setStreamingText(fullResponse);
|
|
245
|
+
}
|
|
246
|
+
else if (event.type === 'tool-result') {
|
|
247
|
+
const resultStr = typeof event.result === 'string' ? event.result : JSON.stringify(event.result, null, 2);
|
|
248
|
+
const shortResult = resultStr
|
|
249
|
+
? (resultStr.length > 200 ? resultStr.slice(0, 200) + '...' : resultStr)
|
|
250
|
+
: '完成';
|
|
251
|
+
fullResponse += ` ✅ ${shortResult}\n`;
|
|
252
|
+
setStreamingText(fullResponse);
|
|
253
|
+
setStatusText('思考中...');
|
|
254
|
+
}
|
|
255
|
+
else if (event.type === 'messages') {
|
|
256
|
+
messagesRef.current = event.messages;
|
|
257
|
+
saveMessages(sessionId, messagesRef.current);
|
|
258
|
+
didGetMessages = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (idleTimer)
|
|
262
|
+
clearTimeout(idleTimer);
|
|
263
|
+
setStatusText('');
|
|
264
|
+
// Move streaming text into Static as a completed message
|
|
265
|
+
if (fullResponse) {
|
|
266
|
+
addDisplayItem('assistant', fullResponse);
|
|
267
|
+
}
|
|
268
|
+
if (!didGetMessages) {
|
|
269
|
+
const fallback = [...newMessages, { role: 'assistant', content: fullResponse }];
|
|
270
|
+
messagesRef.current = fallback;
|
|
271
|
+
saveMessages(sessionId, fallback);
|
|
272
|
+
}
|
|
273
|
+
setStreamingText('');
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
addDisplayItem('assistant', `Error: ${err.message}`);
|
|
277
|
+
setStreamingText('');
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
setIsGenerating(false);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
return (React.createElement(Box, { flexDirection: "column", width: "100%" },
|
|
285
|
+
React.createElement(Static, { items: displayItems }, (item) => (React.createElement(Box, { key: item.id, flexDirection: "column", marginBottom: 0 },
|
|
286
|
+
React.createElement(Text, { bold: true, color: item.role === 'user' ? 'green' : 'blue' }, item.role === 'user' ? 'You' : 'AcmeCode'),
|
|
287
|
+
React.createElement(Text, null, item.text)))),
|
|
288
|
+
isGenerating && !streamingText && statusText ? (React.createElement(Box, { marginY: 0 },
|
|
289
|
+
React.createElement(Spinner, { label: statusText }))) : null,
|
|
290
|
+
isGenerating && streamingText ? (React.createElement(Box, { flexDirection: "column", marginBottom: 0 },
|
|
291
|
+
React.createElement(Text, { bold: true, color: "blue" }, "AcmeCode"),
|
|
292
|
+
React.createElement(Text, null, streamingText),
|
|
293
|
+
statusText ? React.createElement(Spinner, { label: statusText }) : null)) : null,
|
|
294
|
+
!isGenerating ? (React.createElement(Box, null,
|
|
295
|
+
React.createElement(Text, { color: "green" }, "\u276F "),
|
|
296
|
+
React.createElement(TextInput, { value: currentInput, onChange: setCurrentInput, onSubmit: handleSubmit, placeholder: "Ask AcmeCode (or type /exit)..." }))) : null));
|
|
297
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
4
|
+
export default function Spinner({ label }) {
|
|
5
|
+
const [frame, setFrame] = useState(0);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const timer = setInterval(() => {
|
|
8
|
+
setFrame(prev => (prev + 1) % frames.length);
|
|
9
|
+
}, 80);
|
|
10
|
+
return () => clearInterval(timer);
|
|
11
|
+
}, []);
|
|
12
|
+
return (React.createElement(Text, { color: "yellow" },
|
|
13
|
+
frames[frame],
|
|
14
|
+
" ",
|
|
15
|
+
label || 'Thinking...'));
|
|
16
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { Text, useInput } from 'ink';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
/**
|
|
5
|
+
* Custom TextInput that fixes the stale-closure bug in ink-text-input@6.0.0.
|
|
6
|
+
* When Chinese IME sends a phrase (多个字符), the original component's useInput
|
|
7
|
+
* callback captures stale `cursorOffset` and `originalValue`, causing characters
|
|
8
|
+
* to be lost. This version uses refs to always read the latest values.
|
|
9
|
+
*/
|
|
10
|
+
function TextInput({ value, placeholder = '', focus = true, showCursor = true, onChange, onSubmit, }) {
|
|
11
|
+
const [cursorOffset, setCursorOffset] = useState(value.length);
|
|
12
|
+
// Refs to avoid stale closures in useInput callback
|
|
13
|
+
const valueRef = useRef(value);
|
|
14
|
+
const cursorRef = useRef(cursorOffset);
|
|
15
|
+
const onChangeRef = useRef(onChange);
|
|
16
|
+
const onSubmitRef = useRef(onSubmit);
|
|
17
|
+
valueRef.current = value;
|
|
18
|
+
cursorRef.current = cursorOffset;
|
|
19
|
+
onChangeRef.current = onChange;
|
|
20
|
+
onSubmitRef.current = onSubmit;
|
|
21
|
+
// Keep cursor in bounds when value changes externally
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (cursorOffset > value.length) {
|
|
24
|
+
setCursorOffset(value.length);
|
|
25
|
+
}
|
|
26
|
+
}, [value, cursorOffset]);
|
|
27
|
+
useInput((input, key) => {
|
|
28
|
+
if (key.upArrow ||
|
|
29
|
+
key.downArrow ||
|
|
30
|
+
(key.ctrl && input === 'c') ||
|
|
31
|
+
key.tab ||
|
|
32
|
+
(key.shift && key.tab) ||
|
|
33
|
+
key.escape) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const currentValue = valueRef.current;
|
|
37
|
+
const currentCursor = cursorRef.current;
|
|
38
|
+
if (key.return) {
|
|
39
|
+
onSubmitRef.current?.(currentValue);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (key.leftArrow) {
|
|
43
|
+
const next = Math.max(0, currentCursor - 1);
|
|
44
|
+
setCursorOffset(next);
|
|
45
|
+
cursorRef.current = next;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (key.rightArrow) {
|
|
49
|
+
const next = Math.min(currentValue.length, currentCursor + 1);
|
|
50
|
+
setCursorOffset(next);
|
|
51
|
+
cursorRef.current = next;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (key.backspace || key.delete) {
|
|
55
|
+
if (currentCursor > 0) {
|
|
56
|
+
const nextValue = currentValue.slice(0, currentCursor - 1) +
|
|
57
|
+
currentValue.slice(currentCursor);
|
|
58
|
+
const nextCursor = currentCursor - 1;
|
|
59
|
+
setCursorOffset(nextCursor);
|
|
60
|
+
cursorRef.current = nextCursor;
|
|
61
|
+
valueRef.current = nextValue;
|
|
62
|
+
onChangeRef.current(nextValue);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Normal character input (including multi-char IME input)
|
|
67
|
+
const nextValue = currentValue.slice(0, currentCursor) +
|
|
68
|
+
input +
|
|
69
|
+
currentValue.slice(currentCursor);
|
|
70
|
+
const nextCursor = currentCursor + input.length;
|
|
71
|
+
setCursorOffset(nextCursor);
|
|
72
|
+
cursorRef.current = nextCursor;
|
|
73
|
+
valueRef.current = nextValue;
|
|
74
|
+
onChangeRef.current(nextValue);
|
|
75
|
+
}, { isActive: focus });
|
|
76
|
+
// Simplified render — avoid expensive chalk.inverse per-character to reduce flicker
|
|
77
|
+
if (showCursor && focus) {
|
|
78
|
+
if (value.length === 0) {
|
|
79
|
+
if (placeholder) {
|
|
80
|
+
return React.createElement(Text, null, chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1)));
|
|
81
|
+
}
|
|
82
|
+
return React.createElement(Text, null, chalk.inverse(' '));
|
|
83
|
+
}
|
|
84
|
+
// Build rendered string with cursor
|
|
85
|
+
const before = value.slice(0, cursorOffset);
|
|
86
|
+
const cursorChar = cursorOffset < value.length ? value[cursorOffset] : ' ';
|
|
87
|
+
const after = cursorOffset < value.length ? value.slice(cursorOffset + 1) : '';
|
|
88
|
+
return React.createElement(Text, null,
|
|
89
|
+
before,
|
|
90
|
+
chalk.inverse(cursorChar),
|
|
91
|
+
after);
|
|
92
|
+
}
|
|
93
|
+
if (value.length === 0 && placeholder) {
|
|
94
|
+
return React.createElement(Text, null, chalk.grey(placeholder));
|
|
95
|
+
}
|
|
96
|
+
return React.createElement(Text, null, value);
|
|
97
|
+
}
|
|
98
|
+
export default React.memo(TextInput);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface ChatMessage {
|
|
3
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
4
|
+
content: any;
|
|
5
|
+
}
|
|
6
|
+
interface AppProps {
|
|
7
|
+
sessionId: string;
|
|
8
|
+
onExit: () => void;
|
|
9
|
+
}
|
|
10
|
+
export default function App({ sessionId, onExit, }: AppProps): React.ReactElement | null;
|
|
11
|
+
export {};
|