@yivan-lab/pretty-please 1.2.0 → 1.3.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/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 maxContentWidth = Math.max(...lines.map(l => console2.getDisplayWidth(l)));
78
- const boxWidth = Math.max(maxContentWidth + 4, console2.getDisplayWidth('生成命令') + 6, 20);
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.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
- .action(async () => {
169
- const { themes } = await import('../src/ui/theme.js');
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
- Object.keys(themes).forEach((themeName) => {
176
- const isCurrent = themeName === currentTheme;
177
- const prefix = isCurrent ? '●' : '○';
178
- const label = themeName === 'dark' ? 'dark (深色)' : 'light (浅色)';
179
- const color = themeName === 'dark' ? '#00D9FF' : '#0284C7';
180
- if (isCurrent) {
181
- console.log(` ${chalk.hex(color)(prefix)} ${chalk.hex(color).bold(label)} ${chalk.gray('(当前)')}`);
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
- else {
184
- console.log(` ${chalk.gray(prefix)} ${label}`);
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]', '主题名称 (dark, light)')
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 label = currentTheme === 'dark' ? 'dark (深色)' : 'light (浅色)';
199
- const color = currentTheme === 'dark' ? '#00D9FF' : '#0284C7';
200
- console.log('');
201
- console.log(`当前主题: ${chalk.hex(color).bold(label)}`);
202
- console.log('');
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 label = name === 'dark' ? 'dark (深色)' : 'light (浅色)';
212
- const color = name === 'dark' ? '#00D9FF' : '#0284C7';
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.success(`已切换到 ${chalk.hex(color).bold(label)} 主题`);
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
- const chatCmd = program.command('chat').description('AI 对话模式,问答、讲解命令');
792
- chatCmd
793
- .command('clear')
794
- .description('清空对话历史')
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 <问题> 与 AI 对话');
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: options.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 maxContentWidth = Math.max(...lines.map(l => console2.getDisplayWidth(l)));
1270
- const boxWidth = Math.max(maxContentWidth + 4, console2.getDisplayWidth('生成命令') + 6, 20);
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.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yivan-lab/pretty-please",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "AI 驱动的命令行工具,将自然语言转换为可执行的 Shell 命令",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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: debug || false, onChunk })
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
- if (debug && typeof result === 'object' && 'debug' in result && result.debug) {
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 } from '../utils/console.js';
5
5
  /**
6
6
  * CommandBox 组件 - 显示带边框和标题的命令框
7
7
  */
8
8
  export const CommandBox = ({ command, title = '生成命令' }) => {
9
9
  const theme = getCurrentTheme();
10
- const lines = command.split('\n');
10
+ // 获取终端宽度,限制最大宽度
11
+ const termWidth = process.stdout.columns || 80;
11
12
  const titleWidth = getDisplayWidth(title);
12
- const maxContentWidth = Math.max(...lines.map(l => getDisplayWidth(l)));
13
- const boxWidth = Math.max(maxContentWidth + 4, titleWidth + 6, 20);
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.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
- lines.map((line, index) => {
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),
@@ -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
  */
@@ -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: 10,
46
- commandHistoryLimit: 10,
47
- shellHistoryLimit: 15,
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 (!VALID_THEMES.includes(strValue)) {
132
- throw new Error(`theme 必须是以下之一: ${VALID_THEMES.join(', ')}`);
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
- console.log(` ${chalk.hex(colors.primary)('theme')}: ${config.theme === 'dark' ? chalk.hex(colors.primary)('dark (深色)') : chalk.hex(colors.primary)('light (浅色)')}`);
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>>;