@yivan-lab/pretty-please 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +380 -0
- package/bin/pls.js +681 -0
- package/bin/pls.tsx +541 -0
- package/dist/bin/pls.d.ts +2 -0
- package/dist/bin/pls.js +429 -0
- package/dist/src/ai.d.ts +48 -0
- package/dist/src/ai.js +295 -0
- package/dist/src/builtin-detector.d.ts +15 -0
- package/dist/src/builtin-detector.js +83 -0
- package/dist/src/chat-history.d.ts +26 -0
- package/dist/src/chat-history.js +81 -0
- package/dist/src/components/Chat.d.ts +13 -0
- package/dist/src/components/Chat.js +80 -0
- package/dist/src/components/ChatStatus.d.ts +9 -0
- package/dist/src/components/ChatStatus.js +34 -0
- package/dist/src/components/CodeColorizer.d.ts +12 -0
- package/dist/src/components/CodeColorizer.js +82 -0
- package/dist/src/components/CommandBox.d.ts +10 -0
- package/dist/src/components/CommandBox.js +45 -0
- package/dist/src/components/CommandGenerator.d.ts +20 -0
- package/dist/src/components/CommandGenerator.js +116 -0
- package/dist/src/components/ConfigDisplay.d.ts +9 -0
- package/dist/src/components/ConfigDisplay.js +42 -0
- package/dist/src/components/ConfigWizard.d.ts +9 -0
- package/dist/src/components/ConfigWizard.js +72 -0
- package/dist/src/components/ConfirmationPrompt.d.ts +12 -0
- package/dist/src/components/ConfirmationPrompt.js +26 -0
- package/dist/src/components/Duration.d.ts +9 -0
- package/dist/src/components/Duration.js +21 -0
- package/dist/src/components/HistoryDisplay.d.ts +9 -0
- package/dist/src/components/HistoryDisplay.js +51 -0
- package/dist/src/components/HookManager.d.ts +10 -0
- package/dist/src/components/HookManager.js +88 -0
- package/dist/src/components/InlineRenderer.d.ts +12 -0
- package/dist/src/components/InlineRenderer.js +75 -0
- package/dist/src/components/MarkdownDisplay.d.ts +13 -0
- package/dist/src/components/MarkdownDisplay.js +197 -0
- package/dist/src/components/MultiStepCommandGenerator.d.ts +25 -0
- package/dist/src/components/MultiStepCommandGenerator.js +142 -0
- package/dist/src/components/TableRenderer.d.ts +12 -0
- package/dist/src/components/TableRenderer.js +66 -0
- package/dist/src/config.d.ts +29 -0
- package/dist/src/config.js +203 -0
- package/dist/src/history.d.ts +20 -0
- package/dist/src/history.js +113 -0
- package/dist/src/mastra-agent.d.ts +7 -0
- package/dist/src/mastra-agent.js +31 -0
- package/dist/src/multi-step.d.ts +41 -0
- package/dist/src/multi-step.js +67 -0
- package/dist/src/shell-hook.d.ts +35 -0
- package/dist/src/shell-hook.js +348 -0
- package/dist/src/sysinfo.d.ts +15 -0
- package/dist/src/sysinfo.js +52 -0
- package/dist/src/ui/theme.d.ts +26 -0
- package/dist/src/ui/theme.js +31 -0
- package/dist/src/utils/console.d.ts +44 -0
- package/dist/src/utils/console.js +114 -0
- package/package.json +78 -0
- package/src/ai.js +324 -0
- package/src/builtin-detector.js +98 -0
- package/src/chat-history.js +94 -0
- package/src/components/Chat.tsx +122 -0
- package/src/components/ChatStatus.tsx +53 -0
- package/src/components/CodeColorizer.tsx +128 -0
- package/src/components/CommandBox.tsx +60 -0
- package/src/components/CommandGenerator.tsx +184 -0
- package/src/components/ConfigDisplay.tsx +64 -0
- package/src/components/ConfigWizard.tsx +101 -0
- package/src/components/ConfirmationPrompt.tsx +41 -0
- package/src/components/Duration.tsx +24 -0
- package/src/components/HistoryDisplay.tsx +69 -0
- package/src/components/HookManager.tsx +150 -0
- package/src/components/InlineRenderer.tsx +123 -0
- package/src/components/MarkdownDisplay.tsx +288 -0
- package/src/components/MultiStepCommandGenerator.tsx +229 -0
- package/src/components/TableRenderer.tsx +110 -0
- package/src/config.js +221 -0
- package/src/history.js +131 -0
- package/src/mastra-agent.ts +35 -0
- package/src/multi-step.ts +93 -0
- package/src/shell-hook.js +393 -0
- package/src/sysinfo.js +57 -0
- package/src/ui/theme.ts +37 -0
- package/src/utils/console.js +130 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import Spinner from 'ink-spinner'
|
|
4
|
+
import { MarkdownDisplay } from './MarkdownDisplay.js'
|
|
5
|
+
import { chatWithAI } from '../ai.js'
|
|
6
|
+
import { getChatRoundCount } from '../chat-history.js'
|
|
7
|
+
import { theme } from '../ui/theme.js'
|
|
8
|
+
|
|
9
|
+
interface ChatProps {
|
|
10
|
+
prompt: string
|
|
11
|
+
debug?: boolean
|
|
12
|
+
showRoundCount?: boolean
|
|
13
|
+
onComplete: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type Status = 'thinking' | 'streaming' | 'done' | 'error'
|
|
17
|
+
|
|
18
|
+
interface DebugInfo {
|
|
19
|
+
sysinfo: string
|
|
20
|
+
model: string
|
|
21
|
+
systemPrompt: string
|
|
22
|
+
userPrompt: string
|
|
23
|
+
chatHistory: any[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Chat 组件 - AI 对话模式
|
|
28
|
+
* 使用正常渲染,完成后保持最后一帧在终端
|
|
29
|
+
*/
|
|
30
|
+
export function Chat({ prompt, debug, showRoundCount, onComplete }: ChatProps) {
|
|
31
|
+
const [status, setStatus] = useState<Status>('thinking')
|
|
32
|
+
const [content, setContent] = useState('')
|
|
33
|
+
const [duration, setDuration] = useState(0)
|
|
34
|
+
const [debugInfo, setDebugInfo] = useState<DebugInfo | null>(null)
|
|
35
|
+
const [roundCount] = useState(getChatRoundCount())
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const startTime = Date.now()
|
|
39
|
+
|
|
40
|
+
// 流式输出回调
|
|
41
|
+
const onChunk = (chunk: string) => {
|
|
42
|
+
setStatus('streaming')
|
|
43
|
+
setContent((prev) => prev + chunk)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 调用 AI
|
|
47
|
+
chatWithAI(prompt, { debug: debug || false, onChunk })
|
|
48
|
+
.then((result: any) => {
|
|
49
|
+
const endTime = Date.now()
|
|
50
|
+
setDuration(endTime - startTime)
|
|
51
|
+
setStatus('done')
|
|
52
|
+
|
|
53
|
+
if (debug && typeof result === 'object' && 'debug' in result) {
|
|
54
|
+
setDebugInfo(result.debug)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setTimeout(onComplete, 100)
|
|
58
|
+
})
|
|
59
|
+
.catch((error: any) => {
|
|
60
|
+
setStatus('error')
|
|
61
|
+
setContent(error.message)
|
|
62
|
+
setTimeout(onComplete, 100)
|
|
63
|
+
})
|
|
64
|
+
}, [prompt, debug, onComplete])
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Box flexDirection="column">
|
|
68
|
+
{/* 显示对话轮数 */}
|
|
69
|
+
{showRoundCount && roundCount > 0 && (
|
|
70
|
+
<Box marginBottom={1}>
|
|
71
|
+
<Text color={theme.text.secondary}>(对话轮数: {roundCount})</Text>
|
|
72
|
+
</Box>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{/* 动态区域:思考状态 */}
|
|
76
|
+
{status === 'thinking' && (
|
|
77
|
+
<Box>
|
|
78
|
+
<Text color={theme.info}>
|
|
79
|
+
<Spinner type="dots" /> 思考中...
|
|
80
|
+
</Text>
|
|
81
|
+
</Box>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{/* 输出内容区域 */}
|
|
85
|
+
{(status === 'streaming' || status === 'done') && content && (
|
|
86
|
+
<Box marginLeft={2} marginRight={2}>
|
|
87
|
+
<MarkdownDisplay text={content} terminalWidth={96} />
|
|
88
|
+
</Box>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{/* 错误状态 */}
|
|
92
|
+
{status === 'error' && (
|
|
93
|
+
<Box marginTop={1} marginLeft={2}>
|
|
94
|
+
<Text color={theme.error}>❌ 错误: {content}</Text>
|
|
95
|
+
</Box>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{/* 完成后显示耗时 */}
|
|
99
|
+
{status === 'done' && duration > 0 && (
|
|
100
|
+
<Box marginTop={1}>
|
|
101
|
+
<Text color={theme.text.secondary}>({(duration / 1000).toFixed(2)}s)</Text>
|
|
102
|
+
</Box>
|
|
103
|
+
)}
|
|
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
|
+
</Box>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { getChatRoundCount, getChatHistoryFilePath } from '../chat-history.js'
|
|
4
|
+
import { theme } from '../ui/theme.js'
|
|
5
|
+
|
|
6
|
+
interface ChatStatusProps {
|
|
7
|
+
onComplete?: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ChatStatus 组件 - 显示对话状态信息
|
|
12
|
+
*/
|
|
13
|
+
export const ChatStatus: React.FC<ChatStatusProps> = ({ onComplete }) => {
|
|
14
|
+
const roundCount = getChatRoundCount()
|
|
15
|
+
const historyFile = getChatHistoryFilePath()
|
|
16
|
+
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
if (onComplete) {
|
|
19
|
+
setTimeout(onComplete, 100)
|
|
20
|
+
}
|
|
21
|
+
}, [onComplete])
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Box flexDirection="column" marginY={1}>
|
|
25
|
+
<Text bold>💬 AI 对话模式</Text>
|
|
26
|
+
<Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
|
|
27
|
+
|
|
28
|
+
<Box marginTop={1}>
|
|
29
|
+
<Text color={theme.primary}> 当前对话轮数: </Text>
|
|
30
|
+
<Text>{roundCount}</Text>
|
|
31
|
+
</Box>
|
|
32
|
+
|
|
33
|
+
<Box>
|
|
34
|
+
<Text color={theme.primary}> 历史文件: </Text>
|
|
35
|
+
<Text>{historyFile}</Text>
|
|
36
|
+
</Box>
|
|
37
|
+
|
|
38
|
+
<Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
|
|
39
|
+
|
|
40
|
+
<Box flexDirection="column" marginTop={1}>
|
|
41
|
+
<Text color={theme.text.secondary}>用法:</Text>
|
|
42
|
+
<Box>
|
|
43
|
+
<Text color={theme.primary}> pls chat <问题></Text>
|
|
44
|
+
<Text color={theme.text.secondary}> 与 AI 对话</Text>
|
|
45
|
+
</Box>
|
|
46
|
+
<Box>
|
|
47
|
+
<Text color={theme.primary}> pls chat clear</Text>
|
|
48
|
+
<Text color={theme.text.secondary}> 清空对话历史</Text>
|
|
49
|
+
</Box>
|
|
50
|
+
</Box>
|
|
51
|
+
</Box>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Text, Box } from 'ink'
|
|
3
|
+
import { common, createLowlight } from 'lowlight'
|
|
4
|
+
import type { Root, Element, Text as HastText, ElementContent, RootContent } from 'hast'
|
|
5
|
+
import { theme } from '../ui/theme.js'
|
|
6
|
+
|
|
7
|
+
// 创建 lowlight 实例
|
|
8
|
+
const lowlight = createLowlight(common)
|
|
9
|
+
|
|
10
|
+
// 语法高亮颜色映射
|
|
11
|
+
const syntaxColors: Record<string, string> = {
|
|
12
|
+
'hljs-keyword': theme.code.keyword,
|
|
13
|
+
'hljs-string': theme.code.string,
|
|
14
|
+
'hljs-function': theme.code.function,
|
|
15
|
+
'hljs-comment': theme.code.comment,
|
|
16
|
+
'hljs-number': theme.primary,
|
|
17
|
+
'hljs-built_in': theme.secondary,
|
|
18
|
+
'hljs-title': theme.accent,
|
|
19
|
+
'hljs-variable': theme.text.primary,
|
|
20
|
+
'hljs-type': theme.info,
|
|
21
|
+
'hljs-operator': theme.text.secondary,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 渲染 HAST 语法树节点
|
|
26
|
+
*/
|
|
27
|
+
function renderHastNode(
|
|
28
|
+
node: Root | Element | HastText | RootContent,
|
|
29
|
+
inheritedColor: string | undefined
|
|
30
|
+
): React.ReactNode {
|
|
31
|
+
if (node.type === 'text') {
|
|
32
|
+
const color = inheritedColor || theme.code.text
|
|
33
|
+
return <Text color={color}>{node.value}</Text>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (node.type === 'element') {
|
|
37
|
+
const nodeClasses: string[] = (node.properties?.['className'] as string[]) || []
|
|
38
|
+
let elementColor: string | undefined = undefined
|
|
39
|
+
|
|
40
|
+
// 查找颜色
|
|
41
|
+
for (let i = nodeClasses.length - 1; i >= 0; i--) {
|
|
42
|
+
const className = nodeClasses[i]
|
|
43
|
+
if (syntaxColors[className]) {
|
|
44
|
+
elementColor = syntaxColors[className]
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const colorToPassDown = elementColor || inheritedColor
|
|
50
|
+
|
|
51
|
+
// 递归渲染子节点
|
|
52
|
+
const children = node.children?.map((child: ElementContent, index: number) => (
|
|
53
|
+
<React.Fragment key={index}>
|
|
54
|
+
{renderHastNode(child, colorToPassDown)}
|
|
55
|
+
</React.Fragment>
|
|
56
|
+
))
|
|
57
|
+
|
|
58
|
+
return <React.Fragment>{children}</React.Fragment>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (node.type === 'root') {
|
|
62
|
+
if (!node.children || node.children.length === 0) {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return node.children?.map((child: RootContent, index: number) => (
|
|
67
|
+
<React.Fragment key={index}>
|
|
68
|
+
{renderHastNode(child, inheritedColor)}
|
|
69
|
+
</React.Fragment>
|
|
70
|
+
))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 高亮并渲染一行代码
|
|
78
|
+
*/
|
|
79
|
+
function highlightLine(line: string, language: string | null): React.ReactNode {
|
|
80
|
+
try {
|
|
81
|
+
const highlighted = !language || !lowlight.registered(language)
|
|
82
|
+
? lowlight.highlightAuto(line)
|
|
83
|
+
: lowlight.highlight(language, line)
|
|
84
|
+
|
|
85
|
+
const rendered = renderHastNode(highlighted, undefined)
|
|
86
|
+
return rendered !== null ? rendered : line
|
|
87
|
+
} catch {
|
|
88
|
+
return line
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface ColorizeCodeProps {
|
|
93
|
+
code: string
|
|
94
|
+
language?: string | null
|
|
95
|
+
showLineNumbers?: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 代码高亮组件
|
|
100
|
+
*/
|
|
101
|
+
function ColorizeCodeInternal({ code, language = null, showLineNumbers = false }: ColorizeCodeProps) {
|
|
102
|
+
const codeToHighlight = code.replace(/\n$/, '')
|
|
103
|
+
const lines = codeToHighlight.split('\n')
|
|
104
|
+
const padWidth = String(lines.length).length
|
|
105
|
+
|
|
106
|
+
const renderedLines = lines.map((line, index) => {
|
|
107
|
+
const contentToRender = highlightLine(line, language)
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Box key={index} minHeight={1}>
|
|
111
|
+
{showLineNumbers && (
|
|
112
|
+
<Text color={theme.text.dim}>
|
|
113
|
+
{`${String(index + 1).padStart(padWidth, ' ')} `}
|
|
114
|
+
</Text>
|
|
115
|
+
)}
|
|
116
|
+
<Text color={theme.code.text}>{contentToRender}</Text>
|
|
117
|
+
</Box>
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Box flexDirection="column" paddingLeft={1} paddingRight={1} paddingY={1} borderStyle="round" borderColor={theme.border}>
|
|
123
|
+
{renderedLines}
|
|
124
|
+
</Box>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const ColorizeCode = React.memo(ColorizeCodeInternal)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { theme } from '../ui/theme.js'
|
|
4
|
+
|
|
5
|
+
interface CommandBoxProps {
|
|
6
|
+
command: string
|
|
7
|
+
title?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 计算字符串的显示宽度(中文占2个宽度)
|
|
12
|
+
*/
|
|
13
|
+
function getDisplayWidth(str: string): number {
|
|
14
|
+
let width = 0
|
|
15
|
+
for (const char of str) {
|
|
16
|
+
// 中文、日文、韩文等宽字符占 2 个宽度
|
|
17
|
+
if (char.match(/[\u4e00-\u9fff\u3400-\u4dbf\uff00-\uffef\u3000-\u303f]/)) {
|
|
18
|
+
width += 2
|
|
19
|
+
} else {
|
|
20
|
+
width += 1
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return width
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* CommandBox 组件 - 显示带边框和标题的命令框
|
|
28
|
+
*/
|
|
29
|
+
export const CommandBox: React.FC<CommandBoxProps> = ({ command, title = '生成命令' }) => {
|
|
30
|
+
const lines = command.split('\n')
|
|
31
|
+
const titleWidth = getDisplayWidth(title)
|
|
32
|
+
const maxContentWidth = Math.max(...lines.map(l => getDisplayWidth(l)))
|
|
33
|
+
const boxWidth = Math.max(maxContentWidth + 4, titleWidth + 6, 20)
|
|
34
|
+
|
|
35
|
+
// 顶部边框:┌─ 生成命令 ─────┐
|
|
36
|
+
const topPadding = boxWidth - titleWidth - 5
|
|
37
|
+
const topBorder = '┌─ ' + title + ' ' + '─'.repeat(topPadding) + '┐'
|
|
38
|
+
|
|
39
|
+
// 底部边框
|
|
40
|
+
const bottomBorder = '└' + '─'.repeat(boxWidth - 2) + '┘'
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Box flexDirection="column" marginY={1}>
|
|
44
|
+
<Text color={theme.warning}>{topBorder}</Text>
|
|
45
|
+
{lines.map((line, index) => {
|
|
46
|
+
const lineWidth = getDisplayWidth(line)
|
|
47
|
+
const padding = ' '.repeat(boxWidth - lineWidth - 4)
|
|
48
|
+
return (
|
|
49
|
+
<Text key={index}>
|
|
50
|
+
<Text color={theme.warning}>│ </Text>
|
|
51
|
+
<Text color={theme.primary}>{line}</Text>
|
|
52
|
+
<Text>{padding}</Text>
|
|
53
|
+
<Text color={theme.warning}> │</Text>
|
|
54
|
+
</Text>
|
|
55
|
+
)
|
|
56
|
+
})}
|
|
57
|
+
<Text color={theme.warning}>{bottomBorder}</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import Spinner from 'ink-spinner'
|
|
4
|
+
import { generateCommand } from '../ai.js'
|
|
5
|
+
import { detectBuiltin, formatBuiltins } from '../builtin-detector.js'
|
|
6
|
+
import { CommandBox } from './CommandBox.js'
|
|
7
|
+
import { ConfirmationPrompt } from './ConfirmationPrompt.js'
|
|
8
|
+
import { Duration } from './Duration.js'
|
|
9
|
+
import { theme } from '../ui/theme.js'
|
|
10
|
+
|
|
11
|
+
interface CommandGeneratorProps {
|
|
12
|
+
prompt: string
|
|
13
|
+
debug?: boolean
|
|
14
|
+
onComplete: (result: {
|
|
15
|
+
command?: string
|
|
16
|
+
confirmed?: boolean
|
|
17
|
+
cancelled?: boolean
|
|
18
|
+
hasBuiltin?: boolean
|
|
19
|
+
builtins?: string[]
|
|
20
|
+
debugInfo?: any
|
|
21
|
+
error?: string
|
|
22
|
+
}) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type State =
|
|
26
|
+
| { type: 'thinking' }
|
|
27
|
+
| { type: 'showing_command'; command: string; hasBuiltin: boolean; builtins: string[] }
|
|
28
|
+
| { type: 'cancelled'; command: string }
|
|
29
|
+
| { type: 'error'; error: string }
|
|
30
|
+
|
|
31
|
+
interface DebugInfo {
|
|
32
|
+
sysinfo: string
|
|
33
|
+
model: string
|
|
34
|
+
systemPrompt: string
|
|
35
|
+
userPrompt: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* CommandGenerator 组件 - 命令生成和确认(仅用于交互)
|
|
40
|
+
* 不执行命令,执行交给调用方用原生方式处理
|
|
41
|
+
*/
|
|
42
|
+
export const CommandGenerator: React.FC<CommandGeneratorProps> = ({ prompt, debug, onComplete }) => {
|
|
43
|
+
const [state, setState] = useState<State>({ type: 'thinking' })
|
|
44
|
+
const [thinkDuration, setThinkDuration] = useState(0)
|
|
45
|
+
const [debugInfo, setDebugInfo] = useState<DebugInfo | null>(null)
|
|
46
|
+
|
|
47
|
+
// 初始化:调用 AI 生成命令
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const thinkStart = Date.now()
|
|
50
|
+
|
|
51
|
+
generateCommand(prompt, { debug: debug || false })
|
|
52
|
+
.then((result: any) => {
|
|
53
|
+
const command = debug && typeof result === 'object' ? result.command : result
|
|
54
|
+
const thinkEnd = Date.now()
|
|
55
|
+
setThinkDuration(thinkEnd - thinkStart)
|
|
56
|
+
|
|
57
|
+
if (debug && typeof result === 'object' && 'debug' in result) {
|
|
58
|
+
setDebugInfo(result.debug)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 检测 builtin
|
|
62
|
+
const { hasBuiltin, builtins } = detectBuiltin(command)
|
|
63
|
+
|
|
64
|
+
setState({
|
|
65
|
+
type: 'showing_command',
|
|
66
|
+
command,
|
|
67
|
+
hasBuiltin,
|
|
68
|
+
builtins,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// 如果是 builtin,直接完成(不执行)
|
|
72
|
+
if (hasBuiltin) {
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
onComplete({
|
|
75
|
+
command,
|
|
76
|
+
confirmed: false,
|
|
77
|
+
hasBuiltin: true,
|
|
78
|
+
builtins,
|
|
79
|
+
debugInfo: debugInfo || undefined,
|
|
80
|
+
})
|
|
81
|
+
}, 100)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.catch((error: any) => {
|
|
85
|
+
setState({ type: 'error', error: error.message })
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
onComplete({ error: error.message })
|
|
88
|
+
}, 100)
|
|
89
|
+
})
|
|
90
|
+
}, [prompt, debug])
|
|
91
|
+
|
|
92
|
+
// 处理确认
|
|
93
|
+
const handleConfirm = () => {
|
|
94
|
+
if (state.type === 'showing_command') {
|
|
95
|
+
// 返回命令和确认状态,让调用方执行
|
|
96
|
+
onComplete({
|
|
97
|
+
command: state.command,
|
|
98
|
+
confirmed: true,
|
|
99
|
+
debugInfo: debugInfo || undefined,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 处理取消
|
|
105
|
+
const handleCancel = () => {
|
|
106
|
+
if (state.type === 'showing_command') {
|
|
107
|
+
setState({ type: 'cancelled', command: state.command })
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
onComplete({
|
|
110
|
+
command: state.command,
|
|
111
|
+
cancelled: true,
|
|
112
|
+
})
|
|
113
|
+
}, 100)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Box flexDirection="column">
|
|
119
|
+
{/* 思考阶段 */}
|
|
120
|
+
{state.type === 'thinking' && (
|
|
121
|
+
<Box>
|
|
122
|
+
<Text color={theme.info}>
|
|
123
|
+
<Spinner type="dots" /> 正在思考...
|
|
124
|
+
</Text>
|
|
125
|
+
</Box>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{/* 思考完成 */}
|
|
129
|
+
{state.type !== 'thinking' && thinkDuration > 0 && (
|
|
130
|
+
<Box>
|
|
131
|
+
<Text color={theme.success}>✓ 思考完成 </Text>
|
|
132
|
+
<Duration ms={thinkDuration} />
|
|
133
|
+
</Box>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{/* 调试信息 */}
|
|
137
|
+
{debugInfo && (
|
|
138
|
+
<Box flexDirection="column" marginY={1}>
|
|
139
|
+
<Text color={theme.accent}>━━━ 调试信息 ━━━</Text>
|
|
140
|
+
<Text color={theme.text.secondary}>系统信息: {debugInfo.sysinfo}</Text>
|
|
141
|
+
<Text color={theme.text.secondary}>模型: {debugInfo.model}</Text>
|
|
142
|
+
<Text color={theme.text.secondary}>System Prompt:</Text>
|
|
143
|
+
<Text dimColor>{debugInfo.systemPrompt}</Text>
|
|
144
|
+
<Text color={theme.text.secondary}>User Prompt: {debugInfo.userPrompt}</Text>
|
|
145
|
+
<Text color={theme.accent}>━━━━━━━━━━━━━━━━</Text>
|
|
146
|
+
</Box>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{/* 显示命令 */}
|
|
150
|
+
{(state.type === 'showing_command' || state.type === 'cancelled') && (
|
|
151
|
+
<CommandBox command={state.command} />
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Builtin 警告 */}
|
|
155
|
+
{state.type === 'showing_command' && state.hasBuiltin && (
|
|
156
|
+
<Box flexDirection="column" marginY={1}>
|
|
157
|
+
<Text color={theme.error}>
|
|
158
|
+
⚠️ 此命令包含 shell 内置命令({formatBuiltins(state.builtins)}),无法在子进程中生效
|
|
159
|
+
</Text>
|
|
160
|
+
<Text color={theme.warning}>💡 请手动复制到终端执行</Text>
|
|
161
|
+
</Box>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* 确认提示 */}
|
|
165
|
+
{state.type === 'showing_command' && !state.hasBuiltin && (
|
|
166
|
+
<ConfirmationPrompt prompt="执行?" onConfirm={handleConfirm} onCancel={handleCancel} />
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{/* 取消 */}
|
|
170
|
+
{state.type === 'cancelled' && (
|
|
171
|
+
<Box marginTop={1}>
|
|
172
|
+
<Text color={theme.text.secondary}>已取消执行</Text>
|
|
173
|
+
</Box>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{/* 错误 */}
|
|
177
|
+
{state.type === 'error' && (
|
|
178
|
+
<Box marginTop={1}>
|
|
179
|
+
<Text color={theme.error}>❌ 错误: {state.error}</Text>
|
|
180
|
+
</Box>
|
|
181
|
+
)}
|
|
182
|
+
</Box>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { getConfig, maskApiKey } from '../config.js'
|
|
4
|
+
import { theme } from '../ui/theme.js'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import os from 'os'
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILE = path.join(os.homedir(), '.please', 'config.json')
|
|
9
|
+
|
|
10
|
+
interface ConfigDisplayProps {
|
|
11
|
+
onComplete?: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ConfigDisplay 组件 - 显示当前配置
|
|
16
|
+
*/
|
|
17
|
+
export const ConfigDisplay: React.FC<ConfigDisplayProps> = ({ onComplete }) => {
|
|
18
|
+
const config = getConfig()
|
|
19
|
+
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
if (onComplete) {
|
|
22
|
+
setTimeout(onComplete, 100)
|
|
23
|
+
}
|
|
24
|
+
}, [onComplete])
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Box flexDirection="column" marginY={1}>
|
|
28
|
+
<Text bold>当前配置:</Text>
|
|
29
|
+
<Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
|
|
30
|
+
|
|
31
|
+
<Box>
|
|
32
|
+
<Text color={theme.primary}> apiKey: </Text>
|
|
33
|
+
<Text>{maskApiKey(config.apiKey)}</Text>
|
|
34
|
+
</Box>
|
|
35
|
+
|
|
36
|
+
<Box>
|
|
37
|
+
<Text color={theme.primary}> baseUrl: </Text>
|
|
38
|
+
<Text>{config.baseUrl}</Text>
|
|
39
|
+
</Box>
|
|
40
|
+
|
|
41
|
+
<Box>
|
|
42
|
+
<Text color={theme.primary}> model: </Text>
|
|
43
|
+
<Text>{config.model}</Text>
|
|
44
|
+
</Box>
|
|
45
|
+
|
|
46
|
+
<Box>
|
|
47
|
+
<Text color={theme.primary}> shellHook: </Text>
|
|
48
|
+
{config.shellHook ? (
|
|
49
|
+
<Text color={theme.success}>已启用</Text>
|
|
50
|
+
) : (
|
|
51
|
+
<Text color={theme.text.secondary}>未启用</Text>
|
|
52
|
+
)}
|
|
53
|
+
</Box>
|
|
54
|
+
|
|
55
|
+
<Box>
|
|
56
|
+
<Text color={theme.primary}> chatHistoryLimit: </Text>
|
|
57
|
+
<Text>{config.chatHistoryLimit} 轮</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
|
|
60
|
+
<Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
|
|
61
|
+
<Text color={theme.text.secondary}>配置文件: {CONFIG_FILE}</Text>
|
|
62
|
+
</Box>
|
|
63
|
+
)
|
|
64
|
+
}
|