@yivan-lab/pretty-please 1.1.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/README.md +390 -1
- package/bin/pls.tsx +1255 -123
- package/dist/bin/pls.js +1098 -103
- package/dist/package.json +4 -4
- package/dist/src/alias.d.ts +41 -0
- package/dist/src/alias.js +240 -0
- package/dist/src/chat-history.js +10 -1
- package/dist/src/components/Chat.js +54 -26
- package/dist/src/components/CodeColorizer.js +26 -20
- package/dist/src/components/CommandBox.js +19 -8
- package/dist/src/components/ConfirmationPrompt.js +2 -1
- package/dist/src/components/Duration.js +2 -1
- package/dist/src/components/InlineRenderer.js +2 -1
- package/dist/src/components/MarkdownDisplay.js +2 -1
- package/dist/src/components/MultiStepCommandGenerator.d.ts +3 -1
- package/dist/src/components/MultiStepCommandGenerator.js +20 -10
- package/dist/src/components/TableRenderer.js +2 -1
- package/dist/src/config.d.ts +33 -3
- package/dist/src/config.js +83 -34
- 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 +23 -7
- package/dist/src/multi-step.js +45 -26
- package/dist/src/prompts.d.ts +30 -4
- package/dist/src/prompts.js +218 -70
- package/dist/src/remote-history.d.ts +63 -0
- package/dist/src/remote-history.js +315 -0
- package/dist/src/remote.d.ts +113 -0
- package/dist/src/remote.js +634 -0
- package/dist/src/shell-hook.d.ts +58 -0
- package/dist/src/shell-hook.js +295 -26
- package/dist/src/ui/theme.d.ts +60 -23
- package/dist/src/ui/theme.js +544 -22
- package/dist/src/upgrade.d.ts +41 -0
- package/dist/src/upgrade.js +348 -0
- package/dist/src/utils/console.d.ts +4 -0
- package/dist/src/utils/console.js +89 -17
- package/package.json +4 -4
- package/src/alias.ts +301 -0
- package/src/chat-history.ts +11 -1
- package/src/components/Chat.tsx +71 -26
- package/src/components/CodeColorizer.tsx +27 -19
- package/src/components/CommandBox.tsx +26 -8
- package/src/components/ConfirmationPrompt.tsx +2 -1
- package/src/components/Duration.tsx +2 -1
- package/src/components/InlineRenderer.tsx +2 -1
- package/src/components/MarkdownDisplay.tsx +2 -1
- package/src/components/MultiStepCommandGenerator.tsx +25 -11
- package/src/components/TableRenderer.tsx +2 -1
- package/src/config.ts +126 -35
- package/src/mastra-agent.ts +3 -12
- package/src/mastra-chat.ts +40 -34
- package/src/multi-step.ts +62 -30
- package/src/prompts.ts +236 -78
- package/src/remote-history.ts +390 -0
- package/src/remote.ts +800 -0
- package/src/shell-hook.ts +339 -26
- package/src/ui/theme.ts +632 -23
- package/src/upgrade.ts +397 -0
- package/src/utils/console.ts +99 -17
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
|
+
}
|
package/src/chat-history.ts
CHANGED
|
@@ -3,6 +3,15 @@ import path from 'path'
|
|
|
3
3
|
import os from 'os'
|
|
4
4
|
import chalk from 'chalk'
|
|
5
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
|
+
}
|
|
6
15
|
|
|
7
16
|
const CONFIG_DIR = path.join(os.homedir(), '.please')
|
|
8
17
|
const CHAT_HISTORY_FILE = path.join(CONFIG_DIR, 'chat_history.json')
|
|
@@ -104,6 +113,7 @@ export function getChatRoundCount(): number {
|
|
|
104
113
|
export function displayChatHistory(): void {
|
|
105
114
|
const history = getChatHistory()
|
|
106
115
|
const config = getConfig()
|
|
116
|
+
const colors = getColors()
|
|
107
117
|
|
|
108
118
|
if (history.length === 0) {
|
|
109
119
|
console.log('\n' + chalk.gray('暂无对话历史'))
|
|
@@ -120,7 +130,7 @@ export function displayChatHistory(): void {
|
|
|
120
130
|
|
|
121
131
|
userMessages.forEach((msg, index) => {
|
|
122
132
|
const num = index + 1
|
|
123
|
-
console.log(` ${chalk.
|
|
133
|
+
console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${msg.content}`)
|
|
124
134
|
})
|
|
125
135
|
|
|
126
136
|
console.log(chalk.gray('━'.repeat(50)))
|
package/src/components/Chat.tsx
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'
|
|
7
|
-
import {
|
|
6
|
+
import { getChatRoundCount, getChatHistory } from '../chat-history.js'
|
|
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
|
interface ChatProps {
|
|
10
15
|
prompt: string
|
|
@@ -19,7 +24,7 @@ interface DebugInfo {
|
|
|
19
24
|
sysinfo: string
|
|
20
25
|
model: string
|
|
21
26
|
systemPrompt: string
|
|
22
|
-
|
|
27
|
+
userContext: string
|
|
23
28
|
chatHistory: any[]
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -28,12 +33,40 @@ interface DebugInfo {
|
|
|
28
33
|
* 使用正常渲染,完成后保持最后一帧在终端
|
|
29
34
|
*/
|
|
30
35
|
export function Chat({ prompt, debug, showRoundCount, onComplete }: ChatProps) {
|
|
36
|
+
const theme = getCurrentTheme()
|
|
31
37
|
const [status, setStatus] = useState<Status>('thinking')
|
|
32
38
|
const [content, setContent] = useState('')
|
|
33
39
|
const [duration, setDuration] = useState(0)
|
|
34
|
-
const [debugInfo, setDebugInfo] = useState<DebugInfo | null>(null)
|
|
35
40
|
const [roundCount] = useState(getChatRoundCount())
|
|
36
41
|
|
|
42
|
+
// Debug 信息:直接在 useState 初始化时计算(同步)
|
|
43
|
+
const [debugInfo] = useState<DebugInfo | null>(() => {
|
|
44
|
+
if (!debug) return null
|
|
45
|
+
|
|
46
|
+
const config = getConfig()
|
|
47
|
+
const sysinfo = formatSystemInfo()
|
|
48
|
+
const plsHistory = formatHistoryForAI()
|
|
49
|
+
const shellHistory = formatShellHistoryForAI()
|
|
50
|
+
const shellHookEnabled = config.shellHook && getShellHistory().length > 0
|
|
51
|
+
const chatHistory = getChatHistory()
|
|
52
|
+
|
|
53
|
+
const userContext = buildChatUserContext(
|
|
54
|
+
prompt,
|
|
55
|
+
sysinfo,
|
|
56
|
+
plsHistory,
|
|
57
|
+
shellHistory,
|
|
58
|
+
shellHookEnabled
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
sysinfo,
|
|
63
|
+
model: config.model,
|
|
64
|
+
systemPrompt: CHAT_SYSTEM_PROMPT,
|
|
65
|
+
userContext,
|
|
66
|
+
chatHistory,
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
37
70
|
useEffect(() => {
|
|
38
71
|
const startTime = Date.now()
|
|
39
72
|
|
|
@@ -44,17 +77,13 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }: ChatProps) {
|
|
|
44
77
|
}
|
|
45
78
|
|
|
46
79
|
// 调用 AI
|
|
47
|
-
chatWithMastra(prompt, { debug:
|
|
80
|
+
chatWithMastra(prompt, { debug: false, onChunk }) // 不需要 AI 返回 debug
|
|
48
81
|
.then((result) => {
|
|
49
82
|
const endTime = Date.now()
|
|
50
83
|
setDuration(endTime - startTime)
|
|
51
84
|
setStatus('done')
|
|
52
85
|
|
|
53
|
-
|
|
54
|
-
setDebugInfo(result.debug)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
setTimeout(onComplete, 100)
|
|
86
|
+
setTimeout(onComplete, debug ? 500 : 100)
|
|
58
87
|
})
|
|
59
88
|
.catch((error: any) => {
|
|
60
89
|
setStatus('error')
|
|
@@ -65,6 +94,38 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }: ChatProps) {
|
|
|
65
94
|
|
|
66
95
|
return (
|
|
67
96
|
<Box flexDirection="column">
|
|
97
|
+
{/* 调试信息 - 放在最前面 */}
|
|
98
|
+
{debugInfo && (
|
|
99
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
100
|
+
<Text color={theme.accent} bold>━━━ 调试信息 ━━━</Text>
|
|
101
|
+
<Text color={theme.text.secondary}>模型: {debugInfo.model}</Text>
|
|
102
|
+
<Text color={theme.text.secondary}>对话历史轮数: {Math.floor(debugInfo.chatHistory.length / 2)}</Text>
|
|
103
|
+
|
|
104
|
+
{/* 历史对话(只显示用户问题) */}
|
|
105
|
+
{debugInfo.chatHistory.length > 0 && (
|
|
106
|
+
<Box flexDirection="column" marginTop={1}>
|
|
107
|
+
<Text color={theme.text.secondary}>历史对话(用户问题):</Text>
|
|
108
|
+
{debugInfo.chatHistory
|
|
109
|
+
.filter((msg) => msg.role === 'user')
|
|
110
|
+
.slice(-5) // 最多显示最近 5 条
|
|
111
|
+
.map((msg, idx) => (
|
|
112
|
+
<Text key={idx} color={theme.text.muted}>
|
|
113
|
+
{idx + 1}. {msg.content.substring(0, 50)}{msg.content.length > 50 ? '...' : ''}
|
|
114
|
+
</Text>
|
|
115
|
+
))}
|
|
116
|
+
</Box>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{/* User Context */}
|
|
120
|
+
<Box flexDirection="column" marginTop={1}>
|
|
121
|
+
<Text color={theme.text.secondary}>User Context (最新消息):</Text>
|
|
122
|
+
<Text color={theme.text.muted}>{debugInfo.userContext.substring(0, 500)}...</Text>
|
|
123
|
+
</Box>
|
|
124
|
+
|
|
125
|
+
<Text color={theme.accent}>━━━━━━━━━━━━━━━━</Text>
|
|
126
|
+
</Box>
|
|
127
|
+
)}
|
|
128
|
+
|
|
68
129
|
{/* 显示对话轮数 */}
|
|
69
130
|
{showRoundCount && roundCount > 0 && (
|
|
70
131
|
<Box marginBottom={1}>
|
|
@@ -101,22 +162,6 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }: ChatProps) {
|
|
|
101
162
|
<Text color={theme.text.secondary}>({(duration / 1000).toFixed(2)}s)</Text>
|
|
102
163
|
</Box>
|
|
103
164
|
)}
|
|
104
|
-
|
|
105
|
-
{/* 调试信息 */}
|
|
106
|
-
{debugInfo && (
|
|
107
|
-
<Box flexDirection="column" marginY={1}>
|
|
108
|
-
<Text color={theme.accent}>━━━ 调试信息 ━━━</Text>
|
|
109
|
-
<Text color={theme.text.secondary}>系统信息: {debugInfo.sysinfo}</Text>
|
|
110
|
-
<Text color={theme.text.secondary}>模型: {debugInfo.model}</Text>
|
|
111
|
-
<Text color={theme.text.secondary}>
|
|
112
|
-
对话历史轮数: {Math.floor(debugInfo.chatHistory.length / 2)}
|
|
113
|
-
</Text>
|
|
114
|
-
<Text color={theme.text.secondary}>System Prompt:</Text>
|
|
115
|
-
<Text dimColor>{debugInfo.systemPrompt}</Text>
|
|
116
|
-
<Text color={theme.text.secondary}>User Prompt: {debugInfo.userPrompt}</Text>
|
|
117
|
-
<Text color={theme.accent}>━━━━━━━━━━━━━━━━</Text>
|
|
118
|
-
</Box>
|
|
119
|
-
)}
|
|
120
165
|
</Box>
|
|
121
166
|
)
|
|
122
167
|
}
|
|
@@ -2,23 +2,27 @@ import React from 'react'
|
|
|
2
2
|
import { Text, Box } from 'ink'
|
|
3
3
|
import { common, createLowlight } from 'lowlight'
|
|
4
4
|
import type { Root, Element, Text as HastText, ElementContent, RootContent } from 'hast'
|
|
5
|
-
import {
|
|
5
|
+
import { getCurrentTheme, type Theme } from '../ui/theme.js'
|
|
6
6
|
|
|
7
7
|
// 创建 lowlight 实例
|
|
8
8
|
const lowlight = createLowlight(common)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
/**
|
|
11
|
+
* 获取语法高亮颜色映射
|
|
12
|
+
*/
|
|
13
|
+
function getSyntaxColors(theme: Theme): Record<string, string> {
|
|
14
|
+
return {
|
|
15
|
+
'hljs-keyword': theme.code.keyword,
|
|
16
|
+
'hljs-string': theme.code.string,
|
|
17
|
+
'hljs-function': theme.code.function,
|
|
18
|
+
'hljs-comment': theme.code.comment,
|
|
19
|
+
'hljs-number': theme.primary,
|
|
20
|
+
'hljs-built_in': theme.secondary,
|
|
21
|
+
'hljs-title': theme.accent,
|
|
22
|
+
'hljs-variable': theme.text.primary,
|
|
23
|
+
'hljs-type': theme.info,
|
|
24
|
+
'hljs-operator': theme.text.secondary,
|
|
25
|
+
}
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
/**
|
|
@@ -26,7 +30,9 @@ const syntaxColors: Record<string, string> = {
|
|
|
26
30
|
*/
|
|
27
31
|
function renderHastNode(
|
|
28
32
|
node: Root | Element | HastText | RootContent,
|
|
29
|
-
inheritedColor: string | undefined
|
|
33
|
+
inheritedColor: string | undefined,
|
|
34
|
+
syntaxColors: Record<string, string>,
|
|
35
|
+
theme: Theme
|
|
30
36
|
): React.ReactNode {
|
|
31
37
|
if (node.type === 'text') {
|
|
32
38
|
const color = inheritedColor || theme.code.text
|
|
@@ -51,7 +57,7 @@ function renderHastNode(
|
|
|
51
57
|
// 递归渲染子节点
|
|
52
58
|
const children = node.children?.map((child: ElementContent, index: number) => (
|
|
53
59
|
<React.Fragment key={index}>
|
|
54
|
-
{renderHastNode(child, colorToPassDown)}
|
|
60
|
+
{renderHastNode(child, colorToPassDown, syntaxColors, theme)}
|
|
55
61
|
</React.Fragment>
|
|
56
62
|
))
|
|
57
63
|
|
|
@@ -65,7 +71,7 @@ function renderHastNode(
|
|
|
65
71
|
|
|
66
72
|
return node.children?.map((child: RootContent, index: number) => (
|
|
67
73
|
<React.Fragment key={index}>
|
|
68
|
-
{renderHastNode(child, inheritedColor)}
|
|
74
|
+
{renderHastNode(child, inheritedColor, syntaxColors, theme)}
|
|
69
75
|
</React.Fragment>
|
|
70
76
|
))
|
|
71
77
|
}
|
|
@@ -76,13 +82,13 @@ function renderHastNode(
|
|
|
76
82
|
/**
|
|
77
83
|
* 高亮并渲染一行代码
|
|
78
84
|
*/
|
|
79
|
-
function highlightLine(line: string, language: string | null): React.ReactNode {
|
|
85
|
+
function highlightLine(line: string, language: string | null, syntaxColors: Record<string, string>, theme: Theme): React.ReactNode {
|
|
80
86
|
try {
|
|
81
87
|
const highlighted = !language || !lowlight.registered(language)
|
|
82
88
|
? lowlight.highlightAuto(line)
|
|
83
89
|
: lowlight.highlight(language, line)
|
|
84
90
|
|
|
85
|
-
const rendered = renderHastNode(highlighted, undefined)
|
|
91
|
+
const rendered = renderHastNode(highlighted, undefined, syntaxColors, theme)
|
|
86
92
|
return rendered !== null ? rendered : line
|
|
87
93
|
} catch {
|
|
88
94
|
return line
|
|
@@ -99,12 +105,14 @@ interface ColorizeCodeProps {
|
|
|
99
105
|
* 代码高亮组件
|
|
100
106
|
*/
|
|
101
107
|
function ColorizeCodeInternal({ code, language = null, showLineNumbers = false }: ColorizeCodeProps) {
|
|
108
|
+
const theme = getCurrentTheme()
|
|
109
|
+
const syntaxColors = getSyntaxColors(theme)
|
|
102
110
|
const codeToHighlight = code.replace(/\n$/, '')
|
|
103
111
|
const lines = codeToHighlight.split('\n')
|
|
104
112
|
const padWidth = String(lines.length).length
|
|
105
113
|
|
|
106
114
|
const renderedLines = lines.map((line, index) => {
|
|
107
|
-
const contentToRender = highlightLine(line, language)
|
|
115
|
+
const contentToRender = highlightLine(line, language, syntaxColors, theme)
|
|
108
116
|
|
|
109
117
|
return (
|
|
110
118
|
<Box key={index} minHeight={1}>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { Box, Text } from 'ink'
|
|
3
|
-
import {
|
|
4
|
-
import { getDisplayWidth } from '../utils/console.js'
|
|
3
|
+
import { getCurrentTheme } from '../ui/theme.js'
|
|
4
|
+
import { getDisplayWidth, wrapText } from '../utils/console.js'
|
|
5
5
|
|
|
6
6
|
interface CommandBoxProps {
|
|
7
7
|
command: string
|
|
@@ -12,14 +12,32 @@ interface CommandBoxProps {
|
|
|
12
12
|
* CommandBox 组件 - 显示带边框和标题的命令框
|
|
13
13
|
*/
|
|
14
14
|
export const CommandBox: React.FC<CommandBoxProps> = ({ command, title = '生成命令' }) => {
|
|
15
|
-
const
|
|
15
|
+
const theme = getCurrentTheme()
|
|
16
|
+
|
|
17
|
+
// 获取终端宽度,限制最大宽度
|
|
18
|
+
const termWidth = process.stdout.columns || 80
|
|
16
19
|
const titleWidth = getDisplayWidth(title)
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
|
|
21
|
+
// 计算最大内容宽度(终端宽度 - 边框和内边距)
|
|
22
|
+
const maxContentWidth = termWidth - 6 // 减去 '│ ' 和 ' │' 以及一些余量
|
|
23
|
+
|
|
24
|
+
// 处理命令换行
|
|
25
|
+
const originalLines = command.split('\n')
|
|
26
|
+
const wrappedLines: string[] = []
|
|
27
|
+
for (const line of originalLines) {
|
|
28
|
+
wrappedLines.push(...wrapText(line, maxContentWidth))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 计算实际使用的宽度
|
|
32
|
+
const actualMaxWidth = Math.max(
|
|
33
|
+
...wrappedLines.map((l) => getDisplayWidth(l)),
|
|
34
|
+
titleWidth
|
|
35
|
+
)
|
|
36
|
+
const boxWidth = Math.min(actualMaxWidth + 4, termWidth - 2)
|
|
19
37
|
|
|
20
38
|
// 顶部边框:┌─ 生成命令 ─────┐
|
|
21
39
|
const topPadding = boxWidth - titleWidth - 5
|
|
22
|
-
const topBorder = '┌─ ' + title + ' ' + '─'.repeat(topPadding) + '┐'
|
|
40
|
+
const topBorder = '┌─ ' + title + ' ' + '─'.repeat(Math.max(0, topPadding)) + '┐'
|
|
23
41
|
|
|
24
42
|
// 底部边框
|
|
25
43
|
const bottomBorder = '└' + '─'.repeat(boxWidth - 2) + '┘'
|
|
@@ -27,9 +45,9 @@ export const CommandBox: React.FC<CommandBoxProps> = ({ command, title = '生成
|
|
|
27
45
|
return (
|
|
28
46
|
<Box flexDirection="column" marginY={1}>
|
|
29
47
|
<Text color={theme.warning}>{topBorder}</Text>
|
|
30
|
-
{
|
|
48
|
+
{wrappedLines.map((line, index) => {
|
|
31
49
|
const lineWidth = getDisplayWidth(line)
|
|
32
|
-
const padding = ' '.repeat(boxWidth - lineWidth - 4)
|
|
50
|
+
const padding = ' '.repeat(Math.max(0, boxWidth - lineWidth - 4))
|
|
33
51
|
return (
|
|
34
52
|
<Text key={index}>
|
|
35
53
|
<Text color={theme.warning}>│ </Text>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { Text, useInput } from 'ink'
|
|
3
|
-
import {
|
|
3
|
+
import { getCurrentTheme } from '../ui/theme.js'
|
|
4
4
|
|
|
5
5
|
interface ConfirmationPromptProps {
|
|
6
6
|
prompt: string
|
|
@@ -19,6 +19,7 @@ export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = ({
|
|
|
19
19
|
onCancel,
|
|
20
20
|
onEdit,
|
|
21
21
|
}) => {
|
|
22
|
+
const theme = getCurrentTheme()
|
|
22
23
|
useInput((input, key) => {
|
|
23
24
|
if (key.return) {
|
|
24
25
|
// 回车键
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { Text } from 'ink'
|
|
3
|
-
import {
|
|
3
|
+
import { getCurrentTheme } from '../ui/theme.js'
|
|
4
4
|
|
|
5
5
|
interface DurationProps {
|
|
6
6
|
ms: number
|
|
@@ -20,5 +20,6 @@ function formatDuration(ms: number): string {
|
|
|
20
20
|
* Duration 组件 - 显示耗时
|
|
21
21
|
*/
|
|
22
22
|
export const Duration: React.FC<DurationProps> = ({ ms }) => {
|
|
23
|
+
const theme = getCurrentTheme()
|
|
23
24
|
return <Text color={theme.text.secondary}>({formatDuration(ms)})</Text>
|
|
24
25
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { Text } from 'ink'
|
|
3
|
-
import {
|
|
3
|
+
import { getCurrentTheme } from '../ui/theme.js'
|
|
4
4
|
|
|
5
5
|
interface RenderInlineProps {
|
|
6
6
|
text: string
|
|
@@ -12,6 +12,7 @@ interface RenderInlineProps {
|
|
|
12
12
|
* 处理 **粗体**、*斜体*、`代码`、~~删除线~~、<u>下划线</u>、链接
|
|
13
13
|
*/
|
|
14
14
|
function RenderInlineInternal({ text, defaultColor }: RenderInlineProps) {
|
|
15
|
+
const theme = getCurrentTheme()
|
|
15
16
|
const baseColor = defaultColor || theme.text.primary
|
|
16
17
|
|
|
17
18
|
// 快速路径:纯文本无 markdown
|