@yivan-lab/pretty-please 1.0.0 → 1.2.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.
Files changed (89) hide show
  1. package/README.md +381 -28
  2. package/bin/pls.tsx +1138 -109
  3. package/dist/bin/pls.d.ts +1 -1
  4. package/dist/bin/pls.js +994 -91
  5. package/dist/package.json +80 -0
  6. package/dist/src/ai.d.ts +1 -41
  7. package/dist/src/ai.js +9 -190
  8. package/dist/src/alias.d.ts +41 -0
  9. package/dist/src/alias.js +240 -0
  10. package/dist/src/builtin-detector.d.ts +14 -8
  11. package/dist/src/builtin-detector.js +36 -16
  12. package/dist/src/chat-history.d.ts +16 -11
  13. package/dist/src/chat-history.js +35 -4
  14. package/dist/src/components/Chat.js +5 -4
  15. package/dist/src/components/CodeColorizer.js +26 -20
  16. package/dist/src/components/CommandBox.js +3 -17
  17. package/dist/src/components/ConfirmationPrompt.d.ts +2 -1
  18. package/dist/src/components/ConfirmationPrompt.js +9 -4
  19. package/dist/src/components/Duration.js +2 -1
  20. package/dist/src/components/InlineRenderer.js +2 -1
  21. package/dist/src/components/MarkdownDisplay.js +2 -1
  22. package/dist/src/components/MultiStepCommandGenerator.d.ts +5 -1
  23. package/dist/src/components/MultiStepCommandGenerator.js +127 -14
  24. package/dist/src/components/TableRenderer.js +2 -1
  25. package/dist/src/config.d.ts +59 -9
  26. package/dist/src/config.js +147 -48
  27. package/dist/src/history.d.ts +19 -5
  28. package/dist/src/history.js +26 -11
  29. package/dist/src/mastra-agent.d.ts +0 -1
  30. package/dist/src/mastra-agent.js +3 -4
  31. package/dist/src/mastra-chat.d.ts +28 -0
  32. package/dist/src/mastra-chat.js +93 -0
  33. package/dist/src/multi-step.d.ts +23 -7
  34. package/dist/src/multi-step.js +29 -6
  35. package/dist/src/prompts.d.ts +11 -0
  36. package/dist/src/prompts.js +140 -0
  37. package/dist/src/remote-history.d.ts +63 -0
  38. package/dist/src/remote-history.js +315 -0
  39. package/dist/src/remote.d.ts +113 -0
  40. package/dist/src/remote.js +634 -0
  41. package/dist/src/shell-hook.d.ts +87 -12
  42. package/dist/src/shell-hook.js +315 -17
  43. package/dist/src/sysinfo.d.ts +9 -5
  44. package/dist/src/sysinfo.js +2 -2
  45. package/dist/src/ui/theme.d.ts +27 -24
  46. package/dist/src/ui/theme.js +71 -21
  47. package/dist/src/upgrade.d.ts +41 -0
  48. package/dist/src/upgrade.js +348 -0
  49. package/dist/src/utils/console.d.ts +11 -11
  50. package/dist/src/utils/console.js +26 -17
  51. package/package.json +11 -9
  52. package/src/alias.ts +301 -0
  53. package/src/builtin-detector.ts +126 -0
  54. package/src/chat-history.ts +140 -0
  55. package/src/components/Chat.tsx +6 -5
  56. package/src/components/CodeColorizer.tsx +27 -19
  57. package/src/components/CommandBox.tsx +3 -17
  58. package/src/components/ConfirmationPrompt.tsx +11 -3
  59. package/src/components/Duration.tsx +2 -1
  60. package/src/components/InlineRenderer.tsx +2 -1
  61. package/src/components/MarkdownDisplay.tsx +2 -1
  62. package/src/components/MultiStepCommandGenerator.tsx +167 -16
  63. package/src/components/TableRenderer.tsx +2 -1
  64. package/src/config.ts +394 -0
  65. package/src/history.ts +160 -0
  66. package/src/mastra-agent.ts +3 -4
  67. package/src/mastra-chat.ts +124 -0
  68. package/src/multi-step.ts +45 -8
  69. package/src/prompts.ts +154 -0
  70. package/src/remote-history.ts +390 -0
  71. package/src/remote.ts +800 -0
  72. package/src/shell-hook.ts +754 -0
  73. package/src/{sysinfo.js → sysinfo.ts} +28 -16
  74. package/src/ui/theme.ts +101 -24
  75. package/src/upgrade.ts +397 -0
  76. package/src/utils/{console.js → console.ts} +36 -27
  77. package/bin/pls.js +0 -681
  78. package/src/ai.js +0 -324
  79. package/src/builtin-detector.js +0 -98
  80. package/src/chat-history.js +0 -94
  81. package/src/components/ChatStatus.tsx +0 -53
  82. package/src/components/CommandGenerator.tsx +0 -184
  83. package/src/components/ConfigDisplay.tsx +0 -64
  84. package/src/components/ConfigWizard.tsx +0 -101
  85. package/src/components/HistoryDisplay.tsx +0 -69
  86. package/src/components/HookManager.tsx +0 -150
  87. package/src/config.js +0 -221
  88. package/src/history.js +0 -131
  89. package/src/shell-hook.js +0 -393
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yivan-lab/pretty-please",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "AI 驱动的命令行工具,将自然语言转换为可执行的 Shell 命令",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,8 +9,10 @@
9
9
  },
10
10
  "scripts": {
11
11
  "dev": "tsx bin/pls.tsx",
12
- "build": "tsc",
12
+ "build": "tsc && node scripts/postbuild.js",
13
13
  "start": "node dist/bin/pls.js",
14
+ "link:dev": "mkdir -p ~/.local/bin && ln -sf \"$(pwd)/bin/pls.tsx\" ~/.local/bin/pls-dev && echo '✅ pls-dev 已链接到 ~/.local/bin/pls-dev'",
15
+ "unlink:dev": "rm -f ~/.local/bin/pls-dev && echo '✅ pls-dev 已移除'",
14
16
  "prepublishOnly": "pnpm build"
15
17
  },
16
18
  "keywords": [
@@ -29,22 +31,23 @@
29
31
  "license": "MIT",
30
32
  "repository": {
31
33
  "type": "git",
32
- "url": "https://github.com/yivan-lab/pretty-please.git"
34
+ "url": "https://github.com/IvanLark/pretty-please.git"
33
35
  },
34
36
  "bugs": {
35
- "url": "https://github.com/yivan-lab/pretty-please/issues"
37
+ "url": "https://github.com/IvanLark/pretty-please/issues"
36
38
  },
37
- "homepage": "https://github.com/yivan-lab/pretty-please#readme",
39
+ "homepage": "https://github.com/IvanLark/pretty-please#readme",
38
40
  "publishConfig": {
39
41
  "access": "public"
40
42
  },
41
43
  "files": [
42
44
  "bin",
43
- "dist",
45
+ "dist/bin",
46
+ "dist/src",
47
+ "dist/package.json",
44
48
  "src",
45
49
  "README.md",
46
50
  "LICENSE",
47
- "package.json",
48
51
  "tsconfig.json"
49
52
  ],
50
53
  "dependencies": {
@@ -60,8 +63,6 @@
60
63
  "ink-text-input": "^6.0.0",
61
64
  "lowlight": "^3.3.0",
62
65
  "marked": "^17.0.1",
63
- "openai": "^6.10.0",
64
- "ora": "^9.0.0",
65
66
  "react": "^19.2.3",
66
67
  "shiki": "^3.20.0",
67
68
  "string-width": "^8.1.0",
@@ -72,6 +73,7 @@
72
73
  "@types/hast": "^3.0.4",
73
74
  "@types/node": "^25.0.2",
74
75
  "@types/react": "^19.2.7",
76
+ "react-devtools-core": "^7.0.1",
75
77
  "tsx": "^4.21.0",
76
78
  "typescript": "^5.9.3"
77
79
  }
package/src/alias.ts ADDED
@@ -0,0 +1,301 @@
1
+ import chalk from 'chalk'
2
+ import { getConfig, saveConfig, type AliasConfig } from './config.js'
3
+ import { getCurrentTheme } from './ui/theme.js'
4
+
5
+ // 获取主题颜色
6
+ function getColors() {
7
+ const theme = getCurrentTheme()
8
+ return {
9
+ primary: theme.primary,
10
+ secondary: theme.secondary,
11
+ success: theme.success,
12
+ error: theme.error,
13
+ warning: theme.warning,
14
+ muted: theme.text.muted,
15
+ }
16
+ }
17
+
18
+ /**
19
+ * 别名解析结果
20
+ */
21
+ export interface AliasResolveResult {
22
+ resolved: boolean
23
+ prompt: string
24
+ aliasName?: string
25
+ originalInput?: string
26
+ }
27
+
28
+ /**
29
+ * 获取所有别名
30
+ */
31
+ export function getAliases(): Record<string, AliasConfig> {
32
+ const config = getConfig()
33
+ return config.aliases || {}
34
+ }
35
+
36
+ /**
37
+ * 添加别名
38
+ * @param name 别名名称
39
+ * @param prompt 对应的 prompt
40
+ * @param description 可选描述
41
+ * @param reservedCommands 保留的子命令列表(动态传入)
42
+ */
43
+ export function addAlias(
44
+ name: string,
45
+ prompt: string,
46
+ description?: string,
47
+ reservedCommands: string[] = []
48
+ ): void {
49
+ // 验证别名名称
50
+ if (!name || !name.trim()) {
51
+ throw new Error('别名名称不能为空')
52
+ }
53
+
54
+ // 移除可能的 @ 前缀
55
+ const aliasName = name.startsWith('@') ? name.slice(1) : name
56
+
57
+ // 验证别名名称格式(只允许字母、数字、下划线、连字符)
58
+ if (!/^[a-zA-Z0-9_-]+$/.test(aliasName)) {
59
+ throw new Error('别名名称只能包含字母、数字、下划线和连字符')
60
+ }
61
+
62
+ // 检查是否与保留命令冲突
63
+ if (reservedCommands.includes(aliasName)) {
64
+ throw new Error(`"${aliasName}" 是保留的子命令,不能用作别名`)
65
+ }
66
+
67
+ // 验证 prompt
68
+ if (!prompt || !prompt.trim()) {
69
+ throw new Error('prompt 不能为空')
70
+ }
71
+
72
+ const config = getConfig()
73
+ if (!config.aliases) {
74
+ config.aliases = {}
75
+ }
76
+
77
+ config.aliases[aliasName] = {
78
+ prompt: prompt.trim(),
79
+ description: description?.trim(),
80
+ }
81
+
82
+ saveConfig(config)
83
+ }
84
+
85
+ /**
86
+ * 删除别名
87
+ */
88
+ export function removeAlias(name: string): boolean {
89
+ // 移除可能的 @ 前缀
90
+ const aliasName = name.startsWith('@') ? name.slice(1) : name
91
+
92
+ const config = getConfig()
93
+ if (!config.aliases || !config.aliases[aliasName]) {
94
+ return false
95
+ }
96
+
97
+ delete config.aliases[aliasName]
98
+ saveConfig(config)
99
+ return true
100
+ }
101
+
102
+ /**
103
+ * 解析参数模板
104
+ * 支持格式:{{param}} 或 {{param:default}}
105
+ */
106
+ function parseTemplateParams(prompt: string): string[] {
107
+ const regex = /\{\{([^}:]+)(?::[^}]*)?\}\}/g
108
+ const params: string[] = []
109
+ let match
110
+
111
+ while ((match = regex.exec(prompt)) !== null) {
112
+ if (!params.includes(match[1])) {
113
+ params.push(match[1])
114
+ }
115
+ }
116
+
117
+ return params
118
+ }
119
+
120
+ /**
121
+ * 替换模板参数
122
+ * @param prompt 原始 prompt(可能包含模板参数)
123
+ * @param args 用户提供的参数(key=value 或 --key=value 格式)
124
+ */
125
+ function replaceTemplateParams(prompt: string, args: string[]): string {
126
+ // 解析用户参数
127
+ const userParams: Record<string, string> = {}
128
+
129
+ for (const arg of args) {
130
+ // 支持 --key=value 或 key=value 格式
131
+ const cleanArg = arg.startsWith('--') ? arg.slice(2) : arg
132
+ const eqIndex = cleanArg.indexOf('=')
133
+ if (eqIndex > 0) {
134
+ const key = cleanArg.slice(0, eqIndex)
135
+ const value = cleanArg.slice(eqIndex + 1)
136
+ userParams[key] = value
137
+ }
138
+ }
139
+
140
+ // 替换模板参数
141
+ let result = prompt
142
+
143
+ // 匹配 {{param}} 或 {{param:default}}
144
+ result = result.replace(/\{\{([^}:]+)(?::([^}]*))?\}\}/g, (match, param, defaultValue) => {
145
+ if (userParams[param] !== undefined) {
146
+ return userParams[param]
147
+ }
148
+ if (defaultValue !== undefined) {
149
+ return defaultValue
150
+ }
151
+ // 没有提供值也没有默认值,保留原样(后面会报错或让用户补充)
152
+ return match
153
+ })
154
+
155
+ return result
156
+ }
157
+
158
+ /**
159
+ * 检查是否还有未替换的模板参数
160
+ */
161
+ function hasUnresolvedParams(prompt: string): string[] {
162
+ const regex = /\{\{([^}:]+)\}\}/g
163
+ const unresolved: string[] = []
164
+ let match
165
+
166
+ while ((match = regex.exec(prompt)) !== null) {
167
+ unresolved.push(match[1])
168
+ }
169
+
170
+ return unresolved
171
+ }
172
+
173
+ /**
174
+ * 解析别名
175
+ * 支持 `pls disk` 和 `pls @disk` 两种格式
176
+ * @param input 用户输入(可能是别名或普通 prompt)
177
+ * @returns 解析结果
178
+ */
179
+ export function resolveAlias(input: string): AliasResolveResult {
180
+ const parts = input.trim().split(/\s+/)
181
+ if (parts.length === 0) {
182
+ return { resolved: false, prompt: input }
183
+ }
184
+
185
+ let aliasName = parts[0]
186
+ const restArgs = parts.slice(1)
187
+
188
+ // 支持 @ 前缀
189
+ if (aliasName.startsWith('@')) {
190
+ aliasName = aliasName.slice(1)
191
+ }
192
+
193
+ const aliases = getAliases()
194
+ const aliasConfig = aliases[aliasName]
195
+
196
+ if (!aliasConfig) {
197
+ return { resolved: false, prompt: input }
198
+ }
199
+
200
+ // 检查是否有模板参数
201
+ const templateParams = parseTemplateParams(aliasConfig.prompt)
202
+
203
+ let resolvedPrompt: string
204
+
205
+ if (templateParams.length > 0) {
206
+ // 有模板参数,进行替换
207
+ resolvedPrompt = replaceTemplateParams(aliasConfig.prompt, restArgs)
208
+
209
+ // 检查是否还有未替换的必填参数
210
+ const unresolved = hasUnresolvedParams(resolvedPrompt)
211
+ if (unresolved.length > 0) {
212
+ throw new Error(`别名 "${aliasName}" 缺少必填参数: ${unresolved.join(', ')}`)
213
+ }
214
+
215
+ // 过滤掉已用于参数替换的 args,剩余的追加到 prompt
216
+ const usedArgs = restArgs.filter((arg) => {
217
+ const cleanArg = arg.startsWith('--') ? arg.slice(2) : arg
218
+ return cleanArg.includes('=')
219
+ })
220
+ const extraArgs = restArgs.filter((arg) => !usedArgs.includes(arg))
221
+
222
+ if (extraArgs.length > 0) {
223
+ resolvedPrompt = `${resolvedPrompt} ${extraArgs.join(' ')}`
224
+ }
225
+ } else {
226
+ // 没有模板参数,直接追加额外内容
227
+ if (restArgs.length > 0) {
228
+ resolvedPrompt = `${aliasConfig.prompt} ${restArgs.join(' ')}`
229
+ } else {
230
+ resolvedPrompt = aliasConfig.prompt
231
+ }
232
+ }
233
+
234
+ return {
235
+ resolved: true,
236
+ prompt: resolvedPrompt,
237
+ aliasName,
238
+ originalInput: input,
239
+ }
240
+ }
241
+
242
+ /**
243
+ * 显示所有别名
244
+ */
245
+ export function displayAliases(): void {
246
+ const aliases = getAliases()
247
+ const colors = getColors()
248
+ const aliasNames = Object.keys(aliases)
249
+
250
+ console.log('')
251
+
252
+ if (aliasNames.length === 0) {
253
+ console.log(chalk.gray(' 暂无别名'))
254
+ console.log('')
255
+ console.log(chalk.gray(' 使用 pls alias add <name> "<prompt>" 添加别名'))
256
+ console.log('')
257
+ return
258
+ }
259
+
260
+ console.log(chalk.bold('命令别名:'))
261
+ console.log(chalk.gray('━'.repeat(50)))
262
+
263
+ for (const name of aliasNames) {
264
+ const alias = aliases[name]
265
+ const params = parseTemplateParams(alias.prompt)
266
+
267
+ // 别名名称
268
+ let line = ` ${chalk.hex(colors.primary)(name)}`
269
+
270
+ // 如果有参数,显示参数
271
+ if (params.length > 0) {
272
+ line += chalk.gray(` <${params.join('> <')}>`)
273
+ }
274
+
275
+ console.log(line)
276
+
277
+ // prompt 内容
278
+ console.log(` ${chalk.gray('→')} ${alias.prompt}`)
279
+
280
+ // 描述
281
+ if (alias.description) {
282
+ console.log(` ${chalk.gray(alias.description)}`)
283
+ }
284
+
285
+ console.log('')
286
+ }
287
+
288
+ console.log(chalk.gray('━'.repeat(50)))
289
+ console.log(chalk.gray('使用: pls <alias> 或 pls @<alias>'))
290
+ console.log('')
291
+ }
292
+
293
+ /**
294
+ * 获取别名的参数信息(用于帮助显示)
295
+ */
296
+ export function getAliasParams(aliasName: string): string[] {
297
+ const aliases = getAliases()
298
+ const alias = aliases[aliasName]
299
+ if (!alias) return []
300
+ return parseTemplateParams(alias.prompt)
301
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Shell builtin 命令检测器
3
+ *
4
+ * 用于检测命令中是否包含 shell 内置命令(builtin)
5
+ * 这些命令在子进程中执行可能无效或行为异常
6
+ */
7
+
8
+ // Shell 内置命令列表
9
+ const SHELL_BUILTINS: readonly string[] = [
10
+ // 目录相关
11
+ 'cd',
12
+ 'pushd',
13
+ 'popd',
14
+ 'dirs',
15
+
16
+ // 历史相关
17
+ 'history',
18
+
19
+ // 别名和函数
20
+ 'alias',
21
+ 'unalias',
22
+
23
+ // 环境变量
24
+ 'export',
25
+ 'set',
26
+ 'unset',
27
+ 'declare',
28
+ 'local',
29
+ 'readonly',
30
+
31
+ // 脚本执行
32
+ 'source',
33
+ '.',
34
+
35
+ // 任务控制
36
+ 'jobs',
37
+ 'fg',
38
+ 'bg',
39
+ 'disown',
40
+
41
+ // 其他
42
+ 'ulimit',
43
+ 'umask',
44
+ 'builtin',
45
+ 'command',
46
+ 'type',
47
+ 'enable',
48
+ 'hash',
49
+ 'help',
50
+ 'let',
51
+ 'read',
52
+ 'wait',
53
+ 'eval',
54
+ 'exec',
55
+ 'trap',
56
+ 'times',
57
+ 'shopt',
58
+ ]
59
+
60
+ /**
61
+ * Builtin 检测结果
62
+ */
63
+ export interface BuiltinResult {
64
+ hasBuiltin: boolean
65
+ builtins: string[]
66
+ }
67
+
68
+ /**
69
+ * 提取命令中的所有命令名
70
+ */
71
+ function extractCommandNames(command: string): string[] {
72
+ // 按分隔符拆分(&&, ||, ;, |, &, 换行)
73
+ // 注意:| 是管道,两边都在子进程中执行,所以也算
74
+ const parts = command.split(/[;&|]+|\n+/)
75
+
76
+ const commandNames: string[] = []
77
+
78
+ for (const part of parts) {
79
+ const trimmed = part.trim()
80
+ if (!trimmed) continue
81
+
82
+ // 提取第一个单词(命令名)
83
+ // 处理 sudo、env 等前缀
84
+ const words = trimmed.split(/\s+/)
85
+
86
+ // 跳过 sudo、env 等
87
+ let i = 0
88
+ while (i < words.length && ['sudo', 'env', 'nohup', 'nice'].includes(words[i])) {
89
+ i++
90
+ }
91
+
92
+ if (i < words.length) {
93
+ commandNames.push(words[i])
94
+ }
95
+ }
96
+
97
+ return commandNames
98
+ }
99
+
100
+ /**
101
+ * 检测命令中是否包含 builtin
102
+ */
103
+ export function detectBuiltin(command: string): BuiltinResult {
104
+ const commandNames = extractCommandNames(command)
105
+ const foundBuiltins: string[] = []
106
+
107
+ for (const name of commandNames) {
108
+ if (SHELL_BUILTINS.includes(name)) {
109
+ foundBuiltins.push(name)
110
+ }
111
+ }
112
+
113
+ return {
114
+ hasBuiltin: foundBuiltins.length > 0,
115
+ builtins: [...new Set(foundBuiltins)], // 去重
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 格式化 builtin 列表为易读的字符串
121
+ */
122
+ export function formatBuiltins(builtins: string[]): string {
123
+ if (builtins.length === 0) return ''
124
+ if (builtins.length === 1) return builtins[0]
125
+ return builtins.join(', ')
126
+ }
@@ -0,0 +1,140 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import chalk from 'chalk'
5
+ import { getConfig } from './config.js'
6
+ import { getCurrentTheme } from './ui/theme.js'
7
+
8
+ // 获取主题颜色
9
+ function getColors() {
10
+ const theme = getCurrentTheme()
11
+ return {
12
+ primary: theme.primary,
13
+ }
14
+ }
15
+
16
+ const CONFIG_DIR = path.join(os.homedir(), '.please')
17
+ const CHAT_HISTORY_FILE = path.join(CONFIG_DIR, 'chat_history.json')
18
+
19
+ /**
20
+ * 聊天消息
21
+ */
22
+ export interface ChatMessage {
23
+ role: 'user' | 'assistant'
24
+ content: string
25
+ }
26
+
27
+ /**
28
+ * 确保配置目录存在
29
+ */
30
+ function ensureConfigDir(): void {
31
+ if (!fs.existsSync(CONFIG_DIR)) {
32
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
33
+ }
34
+ }
35
+
36
+ /**
37
+ * 读取对话历史
38
+ */
39
+ export function getChatHistory(): ChatMessage[] {
40
+ ensureConfigDir()
41
+
42
+ if (!fs.existsSync(CHAT_HISTORY_FILE)) {
43
+ return []
44
+ }
45
+
46
+ try {
47
+ const content = fs.readFileSync(CHAT_HISTORY_FILE, 'utf-8')
48
+ return JSON.parse(content) as ChatMessage[]
49
+ } catch {
50
+ return []
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 保存对话历史
56
+ */
57
+ function saveChatHistory(history: ChatMessage[]): void {
58
+ ensureConfigDir()
59
+ fs.writeFileSync(CHAT_HISTORY_FILE, JSON.stringify(history, null, 2))
60
+ }
61
+
62
+ /**
63
+ * 添加一轮对话(用户问题 + AI 回答)
64
+ */
65
+ export function addChatMessage(userMessage: string, assistantMessage: string): void {
66
+ const config = getConfig()
67
+ const history = getChatHistory()
68
+
69
+ // 添加新的对话
70
+ history.push({ role: 'user', content: userMessage })
71
+ history.push({ role: 'assistant', content: assistantMessage })
72
+
73
+ // 计算当前轮数(每 2 条消息 = 1 轮)
74
+ const currentRounds = Math.floor(history.length / 2)
75
+ const maxRounds = config.chatHistoryLimit || 10
76
+
77
+ // 如果超出限制,移除最早的对话
78
+ if (currentRounds > maxRounds) {
79
+ // 需要移除的轮数
80
+ const removeRounds = currentRounds - maxRounds
81
+ // 移除最早的 N 轮(N*2 条消息)
82
+ history.splice(0, removeRounds * 2)
83
+ }
84
+
85
+ saveChatHistory(history)
86
+ }
87
+
88
+ /**
89
+ * 清空对话历史
90
+ */
91
+ export function clearChatHistory(): void {
92
+ saveChatHistory([])
93
+ }
94
+
95
+ /**
96
+ * 获取对话历史文件路径
97
+ */
98
+ export function getChatHistoryFilePath(): string {
99
+ return CHAT_HISTORY_FILE
100
+ }
101
+
102
+ /**
103
+ * 获取当前对话轮数
104
+ */
105
+ export function getChatRoundCount(): number {
106
+ const history = getChatHistory()
107
+ return Math.floor(history.length / 2)
108
+ }
109
+
110
+ /**
111
+ * 显示对话历史(只显示用户的 prompt)
112
+ */
113
+ export function displayChatHistory(): void {
114
+ const history = getChatHistory()
115
+ const config = getConfig()
116
+ const colors = getColors()
117
+
118
+ if (history.length === 0) {
119
+ console.log('\n' + chalk.gray('暂无对话历史'))
120
+ console.log('')
121
+ return
122
+ }
123
+
124
+ // 只提取用户消息
125
+ const userMessages = history.filter((msg) => msg.role === 'user')
126
+
127
+ console.log('')
128
+ console.log(chalk.bold(`对话历史(最近 ${userMessages.length} 轮):`))
129
+ console.log(chalk.gray('━'.repeat(50)))
130
+
131
+ userMessages.forEach((msg, index) => {
132
+ const num = index + 1
133
+ console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${msg.content}`)
134
+ })
135
+
136
+ console.log(chalk.gray('━'.repeat(50)))
137
+ console.log(chalk.gray(`配置: 保留最近 ${config.chatHistoryLimit} 轮对话`))
138
+ console.log(chalk.gray(`文件: ${CHAT_HISTORY_FILE}`))
139
+ console.log('')
140
+ }
@@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react'
2
2
  import { Box, Text } from 'ink'
3
3
  import Spinner from 'ink-spinner'
4
4
  import { MarkdownDisplay } from './MarkdownDisplay.js'
5
- import { chatWithAI } from '../ai.js'
5
+ import { chatWithMastra } from '../mastra-chat.js'
6
6
  import { getChatRoundCount } from '../chat-history.js'
7
- import { theme } from '../ui/theme.js'
7
+ import { getCurrentTheme } from '../ui/theme.js'
8
8
 
9
9
  interface ChatProps {
10
10
  prompt: string
@@ -28,6 +28,7 @@ interface DebugInfo {
28
28
  * 使用正常渲染,完成后保持最后一帧在终端
29
29
  */
30
30
  export function Chat({ prompt, debug, showRoundCount, onComplete }: ChatProps) {
31
+ const theme = getCurrentTheme()
31
32
  const [status, setStatus] = useState<Status>('thinking')
32
33
  const [content, setContent] = useState('')
33
34
  const [duration, setDuration] = useState(0)
@@ -44,13 +45,13 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }: ChatProps) {
44
45
  }
45
46
 
46
47
  // 调用 AI
47
- chatWithAI(prompt, { debug: debug || false, onChunk })
48
- .then((result: any) => {
48
+ chatWithMastra(prompt, { debug: debug || false, onChunk })
49
+ .then((result) => {
49
50
  const endTime = Date.now()
50
51
  setDuration(endTime - startTime)
51
52
  setStatus('done')
52
53
 
53
- if (debug && typeof result === 'object' && 'debug' in result) {
54
+ if (debug && typeof result === 'object' && 'debug' in result && result.debug) {
54
55
  setDebugInfo(result.debug)
55
56
  }
56
57