@yivan-lab/pretty-please 1.2.0 → 1.3.1
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/README.md +112 -5
- package/bin/pls.tsx +265 -51
- package/dist/bin/pls.js +234 -49
- package/dist/package.json +1 -1
- package/dist/src/components/Chat.js +52 -25
- package/dist/src/components/CommandBox.js +17 -7
- package/dist/src/config.d.ts +1 -2
- package/dist/src/config.js +18 -9
- package/dist/src/mastra-agent.d.ts +1 -0
- package/dist/src/mastra-agent.js +3 -11
- package/dist/src/mastra-chat.d.ts +13 -6
- package/dist/src/mastra-chat.js +31 -31
- package/dist/src/multi-step.d.ts +2 -2
- package/dist/src/multi-step.js +35 -39
- package/dist/src/prompts.d.ts +30 -4
- package/dist/src/prompts.js +218 -70
- package/dist/src/shell-hook.d.ts +5 -0
- package/dist/src/shell-hook.js +56 -10
- package/dist/src/ui/theme.d.ts +35 -1
- package/dist/src/ui/theme.js +480 -8
- package/dist/src/utils/console.d.ts +9 -0
- package/dist/src/utils/console.js +69 -6
- package/package.json +1 -1
- package/src/components/Chat.tsx +69 -25
- package/src/components/CommandBox.tsx +24 -7
- package/src/config.ts +21 -15
- package/src/mastra-agent.ts +3 -12
- package/src/mastra-chat.ts +40 -34
- package/src/multi-step.ts +40 -45
- package/src/prompts.ts +236 -78
- package/src/shell-hook.ts +71 -10
- package/src/ui/theme.ts +542 -10
- package/src/utils/console.ts +80 -6
package/dist/bin/pls.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
|
+
import path from 'path';
|
|
5
6
|
import { exec } from 'child_process';
|
|
7
|
+
import fs from 'fs';
|
|
6
8
|
import os from 'os';
|
|
7
9
|
import chalk from 'chalk';
|
|
8
10
|
// React 和 Ink 懒加载(只在需要 UI 时加载)
|
|
@@ -72,10 +74,16 @@ function executeCommand(command) {
|
|
|
72
74
|
let stderr = '';
|
|
73
75
|
let hasOutput = false;
|
|
74
76
|
console.log(''); // 空行
|
|
75
|
-
//
|
|
77
|
+
// 计算命令框宽度,让分隔线长度一致(限制终端宽度)
|
|
78
|
+
const termWidth = process.stdout.columns || 80;
|
|
79
|
+
const maxContentWidth = termWidth - 6;
|
|
76
80
|
const lines = command.split('\n');
|
|
77
|
-
const
|
|
78
|
-
|
|
81
|
+
const wrappedLines = [];
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
wrappedLines.push(...console2.wrapText(line, maxContentWidth));
|
|
84
|
+
}
|
|
85
|
+
const actualMaxWidth = Math.max(...wrappedLines.map((l) => console2.getDisplayWidth(l)), console2.getDisplayWidth('生成命令'));
|
|
86
|
+
const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2));
|
|
79
87
|
console2.printSeparator('输出', boxWidth);
|
|
80
88
|
// 使用 bash 并启用 pipefail,确保管道中任何命令失败都能正确返回非零退出码
|
|
81
89
|
const child = exec(`set -o pipefail; ${command}`, { shell: '/bin/bash' });
|
|
@@ -141,11 +149,18 @@ configCmd
|
|
|
141
149
|
configCmd
|
|
142
150
|
.command('set <key> <value>')
|
|
143
151
|
.description('设置配置项 (apiKey, baseUrl, provider, model, shellHook, chatHistoryLimit)')
|
|
144
|
-
.action((key, value) => {
|
|
152
|
+
.action(async (key, value) => {
|
|
145
153
|
try {
|
|
154
|
+
const oldConfig = getConfig();
|
|
155
|
+
const oldShellHistoryLimit = oldConfig.shellHistoryLimit;
|
|
146
156
|
setConfigValue(key, value);
|
|
147
157
|
console.log('');
|
|
148
158
|
console2.success(`已设置 ${key}`);
|
|
159
|
+
// 如果修改了 shellHistoryLimit,自动重装 hook
|
|
160
|
+
if (key === 'shellHistoryLimit') {
|
|
161
|
+
const { reinstallHookForLimitChange } = await import('../src/shell-hook.js');
|
|
162
|
+
await reinstallHookForLimitChange(oldShellHistoryLimit, Number(value));
|
|
163
|
+
}
|
|
149
164
|
console.log('');
|
|
150
165
|
}
|
|
151
166
|
catch (error) {
|
|
@@ -165,41 +180,84 @@ const themeCmd = program.command('theme').description('管理主题');
|
|
|
165
180
|
themeCmd
|
|
166
181
|
.command('list')
|
|
167
182
|
.description('查看所有可用主题')
|
|
168
|
-
.
|
|
169
|
-
|
|
183
|
+
.option('--custom', '只显示自定义主题')
|
|
184
|
+
.option('--builtin', '只显示内置主题')
|
|
185
|
+
.action(async (options) => {
|
|
186
|
+
const { getAllThemeMetadata, isBuiltinTheme } = await import('../src/ui/theme.js');
|
|
170
187
|
const config = getConfig();
|
|
171
188
|
const currentTheme = config.theme || 'dark';
|
|
172
189
|
console.log('');
|
|
173
190
|
console2.title('🎨 可用主题:');
|
|
174
191
|
console2.muted('━'.repeat(50));
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
192
|
+
// 动态获取所有主题元数据
|
|
193
|
+
const allThemes = getAllThemeMetadata();
|
|
194
|
+
// 根据选项过滤主题
|
|
195
|
+
const builtinThemes = allThemes.filter((meta) => isBuiltinTheme(meta.name));
|
|
196
|
+
const customThemes = allThemes.filter((meta) => !isBuiltinTheme(meta.name));
|
|
197
|
+
// 显示内置主题
|
|
198
|
+
if (!options.custom) {
|
|
199
|
+
if (builtinThemes.length > 0) {
|
|
200
|
+
console.log('');
|
|
201
|
+
console2.info('内置主题:');
|
|
202
|
+
builtinThemes.forEach((meta) => {
|
|
203
|
+
const isCurrent = meta.name === currentTheme;
|
|
204
|
+
const prefix = isCurrent ? '●' : '○';
|
|
205
|
+
const label = `${meta.name} (${meta.displayName})`;
|
|
206
|
+
if (isCurrent) {
|
|
207
|
+
console.log(` ${chalk.hex(meta.previewColor)(prefix)} ${chalk.hex(meta.previewColor).bold(label)} ${chalk.gray('(当前)')}`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
console.log(` ${chalk.gray(prefix)} ${label}`);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
182
213
|
}
|
|
183
|
-
|
|
184
|
-
|
|
214
|
+
}
|
|
215
|
+
// 显示自定义主题
|
|
216
|
+
if (!options.builtin) {
|
|
217
|
+
if (customThemes.length > 0) {
|
|
218
|
+
console.log('');
|
|
219
|
+
console2.info('自定义主题:');
|
|
220
|
+
customThemes.forEach((meta) => {
|
|
221
|
+
const isCurrent = meta.name === currentTheme;
|
|
222
|
+
const prefix = isCurrent ? '●' : '○';
|
|
223
|
+
const label = `${meta.name} (${meta.displayName})`;
|
|
224
|
+
const emoji = ' ✨';
|
|
225
|
+
if (isCurrent) {
|
|
226
|
+
console.log(` ${chalk.hex(meta.previewColor)(prefix)} ${chalk.hex(meta.previewColor).bold(label)}${emoji} ${chalk.gray('(当前)')}`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
console.log(` ${chalk.gray(prefix)} ${label}${emoji}`);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
185
232
|
}
|
|
186
|
-
|
|
233
|
+
else if (options.custom) {
|
|
234
|
+
console.log('');
|
|
235
|
+
console2.muted(' 还没有自定义主题');
|
|
236
|
+
console2.muted(' 使用 pls theme create <name> 创建');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
console.log('');
|
|
187
240
|
console2.muted('━'.repeat(50));
|
|
188
241
|
console.log('');
|
|
189
242
|
});
|
|
190
243
|
themeCmd
|
|
191
|
-
.argument('[name]', '主题名称
|
|
244
|
+
.argument('[name]', '主题名称')
|
|
192
245
|
.description('切换主题')
|
|
193
|
-
.action((name) => {
|
|
246
|
+
.action(async (name) => {
|
|
247
|
+
const { getThemeMetadata, getAllThemeMetadata, isValidTheme } = await import('../src/ui/theme.js');
|
|
194
248
|
if (!name) {
|
|
195
249
|
// 显示当前主题
|
|
196
250
|
const config = getConfig();
|
|
197
251
|
const currentTheme = config.theme || 'dark';
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
252
|
+
const meta = getThemeMetadata(currentTheme);
|
|
253
|
+
if (meta) {
|
|
254
|
+
console.log('');
|
|
255
|
+
console.log(`当前主题: ${chalk.hex(meta.previewColor).bold(`${meta.name} (${meta.displayName})`)}`);
|
|
256
|
+
if (meta.description) {
|
|
257
|
+
console2.muted(` ${meta.description}`);
|
|
258
|
+
}
|
|
259
|
+
console.log('');
|
|
260
|
+
}
|
|
203
261
|
console2.muted('使用 pls theme list 查看所有主题');
|
|
204
262
|
console2.muted('使用 pls theme <name> 切换主题');
|
|
205
263
|
console.log('');
|
|
@@ -207,11 +265,80 @@ themeCmd
|
|
|
207
265
|
}
|
|
208
266
|
// 切换主题
|
|
209
267
|
try {
|
|
268
|
+
// 验证主题是否存在
|
|
269
|
+
if (!isValidTheme(name)) {
|
|
270
|
+
const allThemes = getAllThemeMetadata();
|
|
271
|
+
const themeNames = allThemes.map((m) => m.name).join(', ');
|
|
272
|
+
throw new Error(`未知主题 "${name}",可用主题: ${themeNames}`);
|
|
273
|
+
}
|
|
210
274
|
setConfigValue('theme', name);
|
|
211
|
-
const
|
|
212
|
-
|
|
275
|
+
const meta = getThemeMetadata(name);
|
|
276
|
+
if (meta) {
|
|
277
|
+
console.log('');
|
|
278
|
+
console2.success(`已切换到 ${chalk.hex(meta.previewColor).bold(`${meta.name} (${meta.displayName})`)} 主题`);
|
|
279
|
+
if (meta.description) {
|
|
280
|
+
console2.muted(` ${meta.description}`);
|
|
281
|
+
}
|
|
282
|
+
console.log('');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
console.log('');
|
|
287
|
+
console2.error(error.message);
|
|
288
|
+
console.log('');
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
// theme create - 创建主题模板
|
|
293
|
+
themeCmd
|
|
294
|
+
.command('create <name>')
|
|
295
|
+
.description('创建自定义主题模板')
|
|
296
|
+
.option('-d, --display-name <name>', '显示名称')
|
|
297
|
+
.option('-c, --category <type>', '主题类别 (dark 或 light)', 'dark')
|
|
298
|
+
.action(async (name, options) => {
|
|
299
|
+
const { createThemeTemplate } = await import('../src/ui/theme.js');
|
|
300
|
+
try {
|
|
301
|
+
// 验证主题名称格式
|
|
302
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
303
|
+
throw new Error('主题名称只能包含小写字母、数字和连字符');
|
|
304
|
+
}
|
|
305
|
+
// 验证类别
|
|
306
|
+
const category = options.category;
|
|
307
|
+
if (category !== 'dark' && category !== 'light') {
|
|
308
|
+
throw new Error('主题类别必须是 dark 或 light');
|
|
309
|
+
}
|
|
310
|
+
// 创建主题目录
|
|
311
|
+
const themesDir = path.join(os.homedir(), '.please', 'themes');
|
|
312
|
+
if (!fs.existsSync(themesDir)) {
|
|
313
|
+
fs.mkdirSync(themesDir, { recursive: true });
|
|
314
|
+
}
|
|
315
|
+
// 检查主题文件是否已存在
|
|
316
|
+
const themePath = path.join(themesDir, `${name}.json`);
|
|
317
|
+
if (fs.existsSync(themePath)) {
|
|
318
|
+
throw new Error(`主题 "${name}" 已存在`);
|
|
319
|
+
}
|
|
320
|
+
// 创建主题模板
|
|
321
|
+
const displayName = options.displayName || name;
|
|
322
|
+
const template = createThemeTemplate(name, displayName, category);
|
|
323
|
+
// 保存到文件
|
|
324
|
+
fs.writeFileSync(themePath, JSON.stringify(template, null, 2), 'utf-8');
|
|
325
|
+
// 显示成功信息
|
|
326
|
+
console.log('');
|
|
327
|
+
console2.success(`已创建主题模板: ${themePath}`);
|
|
328
|
+
console.log('');
|
|
329
|
+
console2.info('📝 下一步:');
|
|
330
|
+
console.log(` 1. 编辑主题文件修改颜色配置`);
|
|
331
|
+
console2.muted(` vim ${themePath}`);
|
|
332
|
+
console.log('');
|
|
333
|
+
console.log(` 2. 验证主题格式`);
|
|
334
|
+
console2.muted(` pls theme validate ${themePath}`);
|
|
335
|
+
console.log('');
|
|
336
|
+
console.log(` 3. 应用主题查看效果`);
|
|
337
|
+
console2.muted(` pls theme ${name}`);
|
|
213
338
|
console.log('');
|
|
214
|
-
console2.
|
|
339
|
+
console2.info('💡 提示:');
|
|
340
|
+
console2.muted(' - 使用在线工具选择颜色: https://colorhunt.co');
|
|
341
|
+
console2.muted(' - 参考内置主题: pls theme list');
|
|
215
342
|
console.log('');
|
|
216
343
|
}
|
|
217
344
|
catch (error) {
|
|
@@ -221,6 +348,67 @@ themeCmd
|
|
|
221
348
|
process.exit(1);
|
|
222
349
|
}
|
|
223
350
|
});
|
|
351
|
+
// theme validate - 验证主题文件
|
|
352
|
+
themeCmd
|
|
353
|
+
.command('validate <file>')
|
|
354
|
+
.description('验证主题文件格式')
|
|
355
|
+
.action(async (file) => {
|
|
356
|
+
const { validateThemeWithDetails } = await import('../src/ui/theme.js');
|
|
357
|
+
try {
|
|
358
|
+
// 读取主题文件
|
|
359
|
+
const themePath = path.isAbsolute(file) ? file : path.join(process.cwd(), file);
|
|
360
|
+
if (!fs.existsSync(themePath)) {
|
|
361
|
+
throw new Error(`文件不存在: ${themePath}`);
|
|
362
|
+
}
|
|
363
|
+
const content = fs.readFileSync(themePath, 'utf-8');
|
|
364
|
+
const theme = JSON.parse(content);
|
|
365
|
+
// 验证主题
|
|
366
|
+
const result = validateThemeWithDetails(theme);
|
|
367
|
+
console.log('');
|
|
368
|
+
if (result.valid) {
|
|
369
|
+
console2.success('✓ 主题验证通过');
|
|
370
|
+
console.log('');
|
|
371
|
+
if (theme.metadata) {
|
|
372
|
+
console2.info('主题信息:');
|
|
373
|
+
console.log(` 名称: ${theme.metadata.name} (${theme.metadata.displayName})`);
|
|
374
|
+
console.log(` 类别: ${theme.metadata.category}`);
|
|
375
|
+
if (theme.metadata.description) {
|
|
376
|
+
console.log(` 描述: ${theme.metadata.description}`);
|
|
377
|
+
}
|
|
378
|
+
if (theme.metadata.author) {
|
|
379
|
+
console.log(` 作者: ${theme.metadata.author}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
console.log('');
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
console2.error('✗ 主题验证失败');
|
|
386
|
+
console.log('');
|
|
387
|
+
console2.info('错误列表:');
|
|
388
|
+
result.errors.forEach((err, idx) => {
|
|
389
|
+
console.log(` ${idx + 1}. ${err}`);
|
|
390
|
+
});
|
|
391
|
+
console.log('');
|
|
392
|
+
console2.info('修复建议:');
|
|
393
|
+
console2.muted(` 1. 编辑主题文件: vim ${themePath}`);
|
|
394
|
+
console2.muted(' 2. 参考内置主题格式');
|
|
395
|
+
console2.muted(' 3. 确保所有颜色使用 #RRGGBB 格式');
|
|
396
|
+
console.log('');
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
console.log('');
|
|
402
|
+
if (error.message.includes('Unexpected token')) {
|
|
403
|
+
console2.error('JSON 格式错误,请检查文件语法');
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
console2.error(error.message);
|
|
407
|
+
}
|
|
408
|
+
console.log('');
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
224
412
|
// history 子命令
|
|
225
413
|
const historyCmd = program.command('history').description('查看或管理命令历史');
|
|
226
414
|
historyCmd
|
|
@@ -787,22 +975,16 @@ remoteCmd
|
|
|
787
975
|
remoteCmd.action(() => {
|
|
788
976
|
displayRemotes();
|
|
789
977
|
});
|
|
790
|
-
// chat
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
.
|
|
794
|
-
.
|
|
795
|
-
.action(() => {
|
|
796
|
-
clearChatHistory();
|
|
797
|
-
console.log('');
|
|
798
|
-
console2.success('对话历史已清空');
|
|
799
|
-
console.log('');
|
|
800
|
-
});
|
|
801
|
-
// 默认 chat 命令(进行对话)
|
|
802
|
-
chatCmd
|
|
803
|
-
.argument('[prompt...]', '你的问题')
|
|
978
|
+
// chat 命令(AI 对话)
|
|
979
|
+
program
|
|
980
|
+
.command('chat')
|
|
981
|
+
.description('AI 对话模式,问答、讲解命令')
|
|
982
|
+
.argument('[prompt...]', '你的问题(不提供则显示状态)')
|
|
804
983
|
.option('-d, --debug', '显示调试信息')
|
|
805
984
|
.action((promptArgs, options) => {
|
|
985
|
+
// Workaround: Commander.js 14.x 的子命令 option 解析有 bug
|
|
986
|
+
// 直接从 process.argv 检查 --debug
|
|
987
|
+
const debug = process.argv.includes('--debug') || process.argv.includes('-d');
|
|
806
988
|
const prompt = promptArgs.join(' ');
|
|
807
989
|
if (!prompt.trim()) {
|
|
808
990
|
// 没有输入,显示对话状态
|
|
@@ -816,8 +998,8 @@ chatCmd
|
|
|
816
998
|
console2.muted('━'.repeat(40));
|
|
817
999
|
console.log('');
|
|
818
1000
|
console2.muted('用法:');
|
|
819
|
-
console2.info(' pls chat <问题>
|
|
820
|
-
console2.info(' pls chat clear
|
|
1001
|
+
console2.info(' pls chat <问题> 与 AI 对话');
|
|
1002
|
+
console2.info(' pls history chat clear 清空对话历史');
|
|
821
1003
|
console.log('');
|
|
822
1004
|
return;
|
|
823
1005
|
}
|
|
@@ -837,7 +1019,7 @@ chatCmd
|
|
|
837
1019
|
const { Chat } = await import('../src/components/Chat.js');
|
|
838
1020
|
render(React.createElement(Chat, {
|
|
839
1021
|
prompt,
|
|
840
|
-
debug:
|
|
1022
|
+
debug: debug, // 使用 debug 变量
|
|
841
1023
|
showRoundCount: true,
|
|
842
1024
|
onComplete: () => process.exit(0),
|
|
843
1025
|
}));
|
|
@@ -1084,9 +1266,6 @@ program
|
|
|
1084
1266
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1085
1267
|
// 处理步骤结果
|
|
1086
1268
|
if (!stepResult || stepResult.cancelled) {
|
|
1087
|
-
console.log('');
|
|
1088
|
-
console2.muted('已取消执行');
|
|
1089
|
-
console.log('');
|
|
1090
1269
|
process.exit(0);
|
|
1091
1270
|
}
|
|
1092
1271
|
if (stepResult.hasBuiltin) {
|
|
@@ -1264,10 +1443,16 @@ async function executeRemoteCommand(remoteName, command) {
|
|
|
1264
1443
|
const workDir = getRemoteWorkDir(remoteName);
|
|
1265
1444
|
const actualCommand = workDir ? `cd ${workDir} && ${command}` : command;
|
|
1266
1445
|
console.log(''); // 空行
|
|
1267
|
-
//
|
|
1446
|
+
// 计算命令框宽度,让分隔线长度一致(限制终端宽度)
|
|
1447
|
+
const termWidth = process.stdout.columns || 80;
|
|
1448
|
+
const maxContentWidth = termWidth - 6;
|
|
1268
1449
|
const lines = command.split('\n');
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1450
|
+
const wrappedLines = [];
|
|
1451
|
+
for (const line of lines) {
|
|
1452
|
+
wrappedLines.push(...console2.wrapText(line, maxContentWidth));
|
|
1453
|
+
}
|
|
1454
|
+
const actualMaxWidth = Math.max(...wrappedLines.map((l) => console2.getDisplayWidth(l)), console2.getDisplayWidth('生成命令'));
|
|
1455
|
+
const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2));
|
|
1271
1456
|
console2.printSeparator(`远程输出 (${remoteName})`, boxWidth);
|
|
1272
1457
|
try {
|
|
1273
1458
|
const result = await sshExec(remoteName, actualCommand, {
|
package/dist/package.json
CHANGED
|
@@ -3,8 +3,13 @@ import { Box, Text } from 'ink';
|
|
|
3
3
|
import Spinner from 'ink-spinner';
|
|
4
4
|
import { MarkdownDisplay } from './MarkdownDisplay.js';
|
|
5
5
|
import { chatWithMastra } from '../mastra-chat.js';
|
|
6
|
-
import { getChatRoundCount } from '../chat-history.js';
|
|
6
|
+
import { getChatRoundCount, getChatHistory } from '../chat-history.js';
|
|
7
7
|
import { getCurrentTheme } from '../ui/theme.js';
|
|
8
|
+
import { formatSystemInfo } from '../sysinfo.js';
|
|
9
|
+
import { formatHistoryForAI } from '../history.js';
|
|
10
|
+
import { formatShellHistoryForAI, getShellHistory } from '../shell-hook.js';
|
|
11
|
+
import { getConfig } from '../config.js';
|
|
12
|
+
import { CHAT_SYSTEM_PROMPT, buildChatUserContext } from '../prompts.js';
|
|
8
13
|
/**
|
|
9
14
|
* Chat 组件 - AI 对话模式
|
|
10
15
|
* 使用正常渲染,完成后保持最后一帧在终端
|
|
@@ -14,8 +19,26 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }) {
|
|
|
14
19
|
const [status, setStatus] = useState('thinking');
|
|
15
20
|
const [content, setContent] = useState('');
|
|
16
21
|
const [duration, setDuration] = useState(0);
|
|
17
|
-
const [debugInfo, setDebugInfo] = useState(null);
|
|
18
22
|
const [roundCount] = useState(getChatRoundCount());
|
|
23
|
+
// Debug 信息:直接在 useState 初始化时计算(同步)
|
|
24
|
+
const [debugInfo] = useState(() => {
|
|
25
|
+
if (!debug)
|
|
26
|
+
return null;
|
|
27
|
+
const config = getConfig();
|
|
28
|
+
const sysinfo = formatSystemInfo();
|
|
29
|
+
const plsHistory = formatHistoryForAI();
|
|
30
|
+
const shellHistory = formatShellHistoryForAI();
|
|
31
|
+
const shellHookEnabled = config.shellHook && getShellHistory().length > 0;
|
|
32
|
+
const chatHistory = getChatHistory();
|
|
33
|
+
const userContext = buildChatUserContext(prompt, sysinfo, plsHistory, shellHistory, shellHookEnabled);
|
|
34
|
+
return {
|
|
35
|
+
sysinfo,
|
|
36
|
+
model: config.model,
|
|
37
|
+
systemPrompt: CHAT_SYSTEM_PROMPT,
|
|
38
|
+
userContext,
|
|
39
|
+
chatHistory,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
19
42
|
useEffect(() => {
|
|
20
43
|
const startTime = Date.now();
|
|
21
44
|
// 流式输出回调
|
|
@@ -24,15 +47,12 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }) {
|
|
|
24
47
|
setContent((prev) => prev + chunk);
|
|
25
48
|
};
|
|
26
49
|
// 调用 AI
|
|
27
|
-
chatWithMastra(prompt, { debug:
|
|
50
|
+
chatWithMastra(prompt, { debug: false, onChunk }) // 不需要 AI 返回 debug
|
|
28
51
|
.then((result) => {
|
|
29
52
|
const endTime = Date.now();
|
|
30
53
|
setDuration(endTime - startTime);
|
|
31
54
|
setStatus('done');
|
|
32
|
-
|
|
33
|
-
setDebugInfo(result.debug);
|
|
34
|
-
}
|
|
35
|
-
setTimeout(onComplete, 100);
|
|
55
|
+
setTimeout(onComplete, debug ? 500 : 100);
|
|
36
56
|
})
|
|
37
57
|
.catch((error) => {
|
|
38
58
|
setStatus('error');
|
|
@@ -41,6 +61,30 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }) {
|
|
|
41
61
|
});
|
|
42
62
|
}, [prompt, debug, onComplete]);
|
|
43
63
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
64
|
+
debugInfo && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
65
|
+
React.createElement(Text, { color: theme.accent, bold: true }, "\u2501\u2501\u2501 \u8C03\u8BD5\u4FE1\u606F \u2501\u2501\u2501"),
|
|
66
|
+
React.createElement(Text, { color: theme.text.secondary },
|
|
67
|
+
"\u6A21\u578B: ",
|
|
68
|
+
debugInfo.model),
|
|
69
|
+
React.createElement(Text, { color: theme.text.secondary },
|
|
70
|
+
"\u5BF9\u8BDD\u5386\u53F2\u8F6E\u6570: ",
|
|
71
|
+
Math.floor(debugInfo.chatHistory.length / 2)),
|
|
72
|
+
debugInfo.chatHistory.length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
73
|
+
React.createElement(Text, { color: theme.text.secondary }, "\u5386\u53F2\u5BF9\u8BDD\uFF08\u7528\u6237\u95EE\u9898\uFF09:"),
|
|
74
|
+
debugInfo.chatHistory
|
|
75
|
+
.filter((msg) => msg.role === 'user')
|
|
76
|
+
.slice(-5) // 最多显示最近 5 条
|
|
77
|
+
.map((msg, idx) => (React.createElement(Text, { key: idx, color: theme.text.muted },
|
|
78
|
+
idx + 1,
|
|
79
|
+
". ",
|
|
80
|
+
msg.content.substring(0, 50),
|
|
81
|
+
msg.content.length > 50 ? '...' : ''))))),
|
|
82
|
+
React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
83
|
+
React.createElement(Text, { color: theme.text.secondary }, "User Context (\u6700\u65B0\u6D88\u606F):"),
|
|
84
|
+
React.createElement(Text, { color: theme.text.muted },
|
|
85
|
+
debugInfo.userContext.substring(0, 500),
|
|
86
|
+
"...")),
|
|
87
|
+
React.createElement(Text, { color: theme.accent }, "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"))),
|
|
44
88
|
showRoundCount && roundCount > 0 && (React.createElement(Box, { marginBottom: 1 },
|
|
45
89
|
React.createElement(Text, { color: theme.text.secondary },
|
|
46
90
|
"(\u5BF9\u8BDD\u8F6E\u6570: ",
|
|
@@ -60,22 +104,5 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }) {
|
|
|
60
104
|
React.createElement(Text, { color: theme.text.secondary },
|
|
61
105
|
"(",
|
|
62
106
|
(duration / 1000).toFixed(2),
|
|
63
|
-
"s)")))
|
|
64
|
-
debugInfo && (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
65
|
-
React.createElement(Text, { color: theme.accent }, "\u2501\u2501\u2501 \u8C03\u8BD5\u4FE1\u606F \u2501\u2501\u2501"),
|
|
66
|
-
React.createElement(Text, { color: theme.text.secondary },
|
|
67
|
-
"\u7CFB\u7EDF\u4FE1\u606F: ",
|
|
68
|
-
debugInfo.sysinfo),
|
|
69
|
-
React.createElement(Text, { color: theme.text.secondary },
|
|
70
|
-
"\u6A21\u578B: ",
|
|
71
|
-
debugInfo.model),
|
|
72
|
-
React.createElement(Text, { color: theme.text.secondary },
|
|
73
|
-
"\u5BF9\u8BDD\u5386\u53F2\u8F6E\u6570: ",
|
|
74
|
-
Math.floor(debugInfo.chatHistory.length / 2)),
|
|
75
|
-
React.createElement(Text, { color: theme.text.secondary }, "System Prompt:"),
|
|
76
|
-
React.createElement(Text, { dimColor: true }, debugInfo.systemPrompt),
|
|
77
|
-
React.createElement(Text, { color: theme.text.secondary },
|
|
78
|
-
"User Prompt: ",
|
|
79
|
-
debugInfo.userPrompt),
|
|
80
|
-
React.createElement(Text, { color: theme.accent }, "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501")))));
|
|
107
|
+
"s)")))));
|
|
81
108
|
}
|
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { getCurrentTheme } from '../ui/theme.js';
|
|
4
|
-
import { getDisplayWidth } from '../utils/console.js';
|
|
4
|
+
import { getDisplayWidth, wrapText, MIN_COMMAND_BOX_WIDTH } from '../utils/console.js';
|
|
5
5
|
/**
|
|
6
6
|
* CommandBox 组件 - 显示带边框和标题的命令框
|
|
7
7
|
*/
|
|
8
8
|
export const CommandBox = ({ command, title = '生成命令' }) => {
|
|
9
9
|
const theme = getCurrentTheme();
|
|
10
|
-
|
|
10
|
+
// 获取终端宽度,限制最大宽度
|
|
11
|
+
const termWidth = process.stdout.columns || 80;
|
|
11
12
|
const titleWidth = getDisplayWidth(title);
|
|
12
|
-
|
|
13
|
-
const
|
|
13
|
+
// 计算最大内容宽度(终端宽度 - 边框和内边距)
|
|
14
|
+
const maxContentWidth = termWidth - 6; // 减去 '│ ' 和 ' │' 以及一些余量
|
|
15
|
+
// 处理命令换行
|
|
16
|
+
const originalLines = command.split('\n');
|
|
17
|
+
const wrappedLines = [];
|
|
18
|
+
for (const line of originalLines) {
|
|
19
|
+
wrappedLines.push(...wrapText(line, maxContentWidth));
|
|
20
|
+
}
|
|
21
|
+
// 计算实际使用的宽度
|
|
22
|
+
const actualMaxWidth = Math.max(...wrappedLines.map((l) => getDisplayWidth(l)), titleWidth);
|
|
23
|
+
const boxWidth = Math.max(MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2));
|
|
14
24
|
// 顶部边框:┌─ 生成命令 ─────┐
|
|
15
25
|
const topPadding = boxWidth - titleWidth - 5;
|
|
16
|
-
const topBorder = '┌─ ' + title + ' ' + '─'.repeat(topPadding) + '┐';
|
|
26
|
+
const topBorder = '┌─ ' + title + ' ' + '─'.repeat(Math.max(0, topPadding)) + '┐';
|
|
17
27
|
// 底部边框
|
|
18
28
|
const bottomBorder = '└' + '─'.repeat(boxWidth - 2) + '┘';
|
|
19
29
|
return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
20
30
|
React.createElement(Text, { color: theme.warning }, topBorder),
|
|
21
|
-
|
|
31
|
+
wrappedLines.map((line, index) => {
|
|
22
32
|
const lineWidth = getDisplayWidth(line);
|
|
23
|
-
const padding = ' '.repeat(boxWidth - lineWidth - 4);
|
|
33
|
+
const padding = ' '.repeat(Math.max(0, boxWidth - lineWidth - 4));
|
|
24
34
|
return (React.createElement(Text, { key: index },
|
|
25
35
|
React.createElement(Text, { color: theme.warning }, "\u2502 "),
|
|
26
36
|
React.createElement(Text, { color: theme.primary }, line),
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import { type ThemeName } from './ui/theme.js';
|
|
1
2
|
export declare const CONFIG_DIR: string;
|
|
2
3
|
declare const VALID_PROVIDERS: readonly ["openai", "anthropic", "deepseek", "google", "groq", "mistral", "cohere", "fireworks", "together"];
|
|
3
4
|
type Provider = (typeof VALID_PROVIDERS)[number];
|
|
4
5
|
declare const VALID_EDIT_MODES: readonly ["manual", "auto"];
|
|
5
6
|
type EditMode = (typeof VALID_EDIT_MODES)[number];
|
|
6
|
-
declare const VALID_THEMES: readonly ["dark", "light"];
|
|
7
|
-
export type ThemeName = (typeof VALID_THEMES)[number];
|
|
8
7
|
/**
|
|
9
8
|
* 别名配置接口
|
|
10
9
|
*/
|
package/dist/src/config.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import readline from 'readline';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import { getCurrentTheme } from './ui/theme.js';
|
|
6
|
+
import { getCurrentTheme, isValidTheme, getAllThemeMetadata } from './ui/theme.js';
|
|
7
7
|
// 获取主题颜色
|
|
8
8
|
function getColors() {
|
|
9
9
|
const theme = getCurrentTheme();
|
|
@@ -31,8 +31,6 @@ const VALID_PROVIDERS = [
|
|
|
31
31
|
];
|
|
32
32
|
// 编辑模式
|
|
33
33
|
const VALID_EDIT_MODES = ['manual', 'auto'];
|
|
34
|
-
// 主题
|
|
35
|
-
const VALID_THEMES = ['dark', 'light'];
|
|
36
34
|
/**
|
|
37
35
|
* 默认配置
|
|
38
36
|
*/
|
|
@@ -42,9 +40,9 @@ const DEFAULT_CONFIG = {
|
|
|
42
40
|
model: 'gpt-4-turbo',
|
|
43
41
|
provider: 'openai',
|
|
44
42
|
shellHook: false,
|
|
45
|
-
chatHistoryLimit:
|
|
46
|
-
commandHistoryLimit:
|
|
47
|
-
shellHistoryLimit:
|
|
43
|
+
chatHistoryLimit: 5,
|
|
44
|
+
commandHistoryLimit: 5,
|
|
45
|
+
shellHistoryLimit: 10,
|
|
48
46
|
editMode: 'manual',
|
|
49
47
|
theme: 'dark',
|
|
50
48
|
aliases: {},
|
|
@@ -128,8 +126,10 @@ export function setConfigValue(key, value) {
|
|
|
128
126
|
}
|
|
129
127
|
else if (key === 'theme') {
|
|
130
128
|
const strValue = String(value);
|
|
131
|
-
if (!
|
|
132
|
-
|
|
129
|
+
if (!isValidTheme(strValue)) {
|
|
130
|
+
const allThemes = getAllThemeMetadata();
|
|
131
|
+
const themeNames = allThemes.map((m) => m.name).join(', ');
|
|
132
|
+
throw new Error(`theme 必须是以下之一: ${themeNames}`);
|
|
133
133
|
}
|
|
134
134
|
config.theme = strValue;
|
|
135
135
|
}
|
|
@@ -173,7 +173,10 @@ export function displayConfig() {
|
|
|
173
173
|
console.log(` ${chalk.hex(colors.primary)('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`);
|
|
174
174
|
console.log(` ${chalk.hex(colors.primary)('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`);
|
|
175
175
|
console.log(` ${chalk.hex(colors.primary)('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`);
|
|
176
|
-
|
|
176
|
+
// 动态显示主题信息
|
|
177
|
+
const themeMetadata = getAllThemeMetadata().find((m) => m.name === config.theme);
|
|
178
|
+
const themeLabel = themeMetadata ? `${themeMetadata.name} (${themeMetadata.displayName})` : config.theme;
|
|
179
|
+
console.log(` ${chalk.hex(colors.primary)('theme')}: ${chalk.hex(colors.primary)(themeLabel)}`);
|
|
177
180
|
console.log(chalk.gray('━'.repeat(50)));
|
|
178
181
|
console.log(chalk.gray(`配置文件: ${CONFIG_FILE}\n`));
|
|
179
182
|
}
|
|
@@ -277,6 +280,7 @@ export async function runConfigWizard() {
|
|
|
277
280
|
}
|
|
278
281
|
}
|
|
279
282
|
// 9. Shell History Limit
|
|
283
|
+
const oldShellHistoryLimit = config.shellHistoryLimit; // 保存旧值
|
|
280
284
|
const shellHistoryPrompt = `${chalk.hex(colors.primary)('Shell 历史保留条数')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.shellHistoryLimit)} ${chalk.gray('→')} `;
|
|
281
285
|
const shellHistoryLimit = await question(rl, shellHistoryPrompt);
|
|
282
286
|
if (shellHistoryLimit.trim()) {
|
|
@@ -290,6 +294,11 @@ export async function runConfigWizard() {
|
|
|
290
294
|
console.log(chalk.hex(getColors().success)('✅ 配置已保存'));
|
|
291
295
|
console.log(chalk.gray(` ${CONFIG_FILE}`));
|
|
292
296
|
console.log();
|
|
297
|
+
// 如果修改了 shellHistoryLimit,自动重装 hook
|
|
298
|
+
if (oldShellHistoryLimit !== config.shellHistoryLimit) {
|
|
299
|
+
const { reinstallHookForLimitChange } = await import('./shell-hook.js');
|
|
300
|
+
await reinstallHookForLimitChange(oldShellHistoryLimit, config.shellHistoryLimit);
|
|
301
|
+
}
|
|
293
302
|
}
|
|
294
303
|
catch (error) {
|
|
295
304
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2,5 +2,6 @@ import { Agent } from '@mastra/core';
|
|
|
2
2
|
/**
|
|
3
3
|
* 创建 Mastra Shell Agent
|
|
4
4
|
* 根据用户配置的 API Key、Base URL、Provider 和 Model
|
|
5
|
+
* 使用静态的 System Prompt(不包含动态数据)
|
|
5
6
|
*/
|
|
6
7
|
export declare function createShellAgent(): Agent<"shell-commander", Record<string, import("@mastra/core").ToolAction<any, any, any, any, import("@mastra/core").ToolExecutionContext<any, any, any>>>, Record<string, import("@mastra/core").Metric>>;
|