@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,288 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Text, Box } from 'ink'
|
|
3
|
+
import { theme } from '../ui/theme.js'
|
|
4
|
+
import { ColorizeCode } from './CodeColorizer.js'
|
|
5
|
+
import { TableRenderer } from './TableRenderer.js'
|
|
6
|
+
import { RenderInline } from './InlineRenderer.js'
|
|
7
|
+
|
|
8
|
+
interface MarkdownDisplayProps {
|
|
9
|
+
text: string
|
|
10
|
+
terminalWidth?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Markdown 主渲染组件
|
|
15
|
+
* 参考 gemini-cli 的实现
|
|
16
|
+
* 支持:标题、列表、代码块、表格、粗体、斜体、链接
|
|
17
|
+
*/
|
|
18
|
+
function MarkdownDisplayInternal({ text, terminalWidth = 80 }: MarkdownDisplayProps) {
|
|
19
|
+
if (!text) return <></>
|
|
20
|
+
|
|
21
|
+
const lines = text.split(/\r?\n/)
|
|
22
|
+
|
|
23
|
+
// 正则表达式
|
|
24
|
+
const headerRegex = /^ *(#{1,4}) +(.*)/
|
|
25
|
+
const codeFenceRegex = /^ *(`{3,}|~{3,}) *(\w*?) *$/
|
|
26
|
+
const ulItemRegex = /^([ \t]*)([-*+]) +(.*)/
|
|
27
|
+
const olItemRegex = /^([ \t]*)(\d+)\. +(.*)/
|
|
28
|
+
const hrRegex = /^ *([-*_] *){3,} *$/
|
|
29
|
+
// 表格行:支持有无尾部 | 的情况
|
|
30
|
+
const tableRowRegex = /^\s*\|(.+?)(?:\|)?\s*$/
|
|
31
|
+
const tableSeparatorRegex = /^\s*\|?\s*(:?-+:?)\s*(\|\s*(:?-+:?)\s*)+\|?\s*$/
|
|
32
|
+
|
|
33
|
+
const contentBlocks: React.ReactNode[] = []
|
|
34
|
+
let inCodeBlock = false
|
|
35
|
+
let lastLineEmpty = true
|
|
36
|
+
let codeBlockContent: string[] = []
|
|
37
|
+
let codeBlockLang: string | null = null
|
|
38
|
+
let codeBlockFence = ''
|
|
39
|
+
let inTable = false
|
|
40
|
+
let tableRows: string[][] = []
|
|
41
|
+
let tableHeaders: string[] = []
|
|
42
|
+
|
|
43
|
+
function addContentBlock(block: React.ReactNode) {
|
|
44
|
+
if (block) {
|
|
45
|
+
contentBlocks.push(block)
|
|
46
|
+
lastLineEmpty = false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
lines.forEach((line, index) => {
|
|
51
|
+
const key = `line-${index}`
|
|
52
|
+
|
|
53
|
+
// 代码块内部
|
|
54
|
+
if (inCodeBlock) {
|
|
55
|
+
const fenceMatch = line.match(codeFenceRegex)
|
|
56
|
+
if (
|
|
57
|
+
fenceMatch &&
|
|
58
|
+
fenceMatch[1].startsWith(codeBlockFence[0]) &&
|
|
59
|
+
fenceMatch[1].length >= codeBlockFence.length
|
|
60
|
+
) {
|
|
61
|
+
// 代码块结束
|
|
62
|
+
addContentBlock(
|
|
63
|
+
<ColorizeCode
|
|
64
|
+
key={key}
|
|
65
|
+
code={codeBlockContent.join('\n')}
|
|
66
|
+
language={codeBlockLang}
|
|
67
|
+
showLineNumbers={false}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
inCodeBlock = false
|
|
71
|
+
codeBlockContent = []
|
|
72
|
+
codeBlockLang = null
|
|
73
|
+
codeBlockFence = ''
|
|
74
|
+
} else {
|
|
75
|
+
codeBlockContent.push(line)
|
|
76
|
+
}
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const codeFenceMatch = line.match(codeFenceRegex)
|
|
81
|
+
const headerMatch = line.match(headerRegex)
|
|
82
|
+
const ulMatch = line.match(ulItemRegex)
|
|
83
|
+
const olMatch = line.match(olItemRegex)
|
|
84
|
+
const hrMatch = line.match(hrRegex)
|
|
85
|
+
const tableRowMatch = line.match(tableRowRegex)
|
|
86
|
+
const tableSeparatorMatch = line.match(tableSeparatorRegex)
|
|
87
|
+
|
|
88
|
+
// 代码块开始
|
|
89
|
+
if (codeFenceMatch) {
|
|
90
|
+
inCodeBlock = true
|
|
91
|
+
codeBlockFence = codeFenceMatch[1]
|
|
92
|
+
codeBlockLang = codeFenceMatch[2] || null
|
|
93
|
+
}
|
|
94
|
+
// 表格开始
|
|
95
|
+
else if (tableRowMatch && !inTable) {
|
|
96
|
+
if (index + 1 < lines.length && lines[index + 1].match(tableSeparatorRegex)) {
|
|
97
|
+
inTable = true
|
|
98
|
+
tableHeaders = tableRowMatch[1].split('|').map(cell => cell.trim())
|
|
99
|
+
tableRows = []
|
|
100
|
+
} else {
|
|
101
|
+
addContentBlock(
|
|
102
|
+
<Box key={key}>
|
|
103
|
+
<Text wrap="wrap" color={theme.text.primary}>
|
|
104
|
+
<RenderInline text={line} defaultColor={theme.text.primary} />
|
|
105
|
+
</Text>
|
|
106
|
+
</Box>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// 表格分隔符
|
|
111
|
+
else if (inTable && tableSeparatorMatch) {
|
|
112
|
+
// 跳过分隔符行
|
|
113
|
+
}
|
|
114
|
+
// 表格行
|
|
115
|
+
else if (inTable && tableRowMatch) {
|
|
116
|
+
const cells = tableRowMatch[1].split('|').map(cell => cell.trim())
|
|
117
|
+
while (cells.length < tableHeaders.length) cells.push('')
|
|
118
|
+
if (cells.length > tableHeaders.length) cells.length = tableHeaders.length
|
|
119
|
+
tableRows.push(cells)
|
|
120
|
+
}
|
|
121
|
+
// 表格结束
|
|
122
|
+
else if (inTable && !tableRowMatch) {
|
|
123
|
+
if (tableHeaders.length > 0 && tableRows.length > 0) {
|
|
124
|
+
addContentBlock(
|
|
125
|
+
<TableRenderer
|
|
126
|
+
key={`table-${contentBlocks.length}`}
|
|
127
|
+
headers={tableHeaders}
|
|
128
|
+
rows={tableRows}
|
|
129
|
+
terminalWidth={terminalWidth}
|
|
130
|
+
/>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
inTable = false
|
|
134
|
+
tableRows = []
|
|
135
|
+
tableHeaders = []
|
|
136
|
+
|
|
137
|
+
// 处理当前行
|
|
138
|
+
if (line.trim().length > 0) {
|
|
139
|
+
addContentBlock(
|
|
140
|
+
<Box key={key}>
|
|
141
|
+
<Text wrap="wrap" color={theme.text.primary}>
|
|
142
|
+
<RenderInline text={line} defaultColor={theme.text.primary} />
|
|
143
|
+
</Text>
|
|
144
|
+
</Box>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// 横线
|
|
149
|
+
else if (hrMatch) {
|
|
150
|
+
addContentBlock(
|
|
151
|
+
<Box key={key}>
|
|
152
|
+
<Text color={theme.border}>{'─'.repeat(40)}</Text>
|
|
153
|
+
</Box>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
// 标题
|
|
157
|
+
else if (headerMatch) {
|
|
158
|
+
const level = headerMatch[1].length
|
|
159
|
+
const headerText = headerMatch[2]
|
|
160
|
+
let headerNode: React.ReactNode = null
|
|
161
|
+
|
|
162
|
+
switch (level) {
|
|
163
|
+
case 1:
|
|
164
|
+
headerNode = (
|
|
165
|
+
<Text bold underline color={theme.primary}>
|
|
166
|
+
<RenderInline text={headerText} defaultColor={theme.primary} />
|
|
167
|
+
</Text>
|
|
168
|
+
)
|
|
169
|
+
break
|
|
170
|
+
case 2:
|
|
171
|
+
headerNode = (
|
|
172
|
+
<Text bold color={theme.secondary}>
|
|
173
|
+
<RenderInline text={headerText} defaultColor={theme.secondary} />
|
|
174
|
+
</Text>
|
|
175
|
+
)
|
|
176
|
+
break
|
|
177
|
+
case 3:
|
|
178
|
+
headerNode = (
|
|
179
|
+
<Text bold color={theme.info}>
|
|
180
|
+
<RenderInline text={headerText} defaultColor={theme.info} />
|
|
181
|
+
</Text>
|
|
182
|
+
)
|
|
183
|
+
break
|
|
184
|
+
default:
|
|
185
|
+
headerNode = (
|
|
186
|
+
<Text bold color={theme.text.primary}>
|
|
187
|
+
<RenderInline text={headerText} defaultColor={theme.text.primary} />
|
|
188
|
+
</Text>
|
|
189
|
+
)
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (headerNode) {
|
|
194
|
+
addContentBlock(
|
|
195
|
+
<Box key={key} marginTop={lastLineEmpty ? 0 : 1}>
|
|
196
|
+
{headerNode}
|
|
197
|
+
</Box>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// 无序列表
|
|
202
|
+
else if (ulMatch) {
|
|
203
|
+
const leadingWhitespace = ulMatch[1]
|
|
204
|
+
const itemText = ulMatch[3]
|
|
205
|
+
const indentation = leadingWhitespace.length
|
|
206
|
+
|
|
207
|
+
addContentBlock(
|
|
208
|
+
<Box key={key} paddingLeft={indentation + 1} flexDirection="row">
|
|
209
|
+
<Box width={2}>
|
|
210
|
+
<Text color={theme.text.primary}>• </Text>
|
|
211
|
+
</Box>
|
|
212
|
+
<Box flexGrow={1}>
|
|
213
|
+
<Text wrap="wrap" color={theme.text.primary}>
|
|
214
|
+
<RenderInline text={itemText} defaultColor={theme.text.primary} />
|
|
215
|
+
</Text>
|
|
216
|
+
</Box>
|
|
217
|
+
</Box>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
// 有序列表
|
|
221
|
+
else if (olMatch) {
|
|
222
|
+
const leadingWhitespace = olMatch[1]
|
|
223
|
+
const marker = olMatch[2]
|
|
224
|
+
const itemText = olMatch[3]
|
|
225
|
+
const indentation = leadingWhitespace.length
|
|
226
|
+
const prefix = `${marker}. `
|
|
227
|
+
|
|
228
|
+
addContentBlock(
|
|
229
|
+
<Box key={key} paddingLeft={indentation + 1} flexDirection="row">
|
|
230
|
+
<Box width={prefix.length}>
|
|
231
|
+
<Text color={theme.text.primary}>{prefix}</Text>
|
|
232
|
+
</Box>
|
|
233
|
+
<Box flexGrow={1}>
|
|
234
|
+
<Text wrap="wrap" color={theme.text.primary}>
|
|
235
|
+
<RenderInline text={itemText} defaultColor={theme.text.primary} />
|
|
236
|
+
</Text>
|
|
237
|
+
</Box>
|
|
238
|
+
</Box>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
// 空行或普通文本
|
|
242
|
+
else {
|
|
243
|
+
if (line.trim().length === 0 && !inCodeBlock) {
|
|
244
|
+
// 空行:不添加额外的 Box,让段落自然分隔
|
|
245
|
+
if (!lastLineEmpty) {
|
|
246
|
+
lastLineEmpty = true
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
const inlineContent = <RenderInline text={line} defaultColor={theme.text.primary} />
|
|
250
|
+
addContentBlock(
|
|
251
|
+
<Box key={key}>
|
|
252
|
+
<Text wrap="wrap" color={theme.text.primary}>
|
|
253
|
+
{inlineContent}
|
|
254
|
+
</Text>
|
|
255
|
+
</Box>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// 处理未闭合的代码块
|
|
262
|
+
if (inCodeBlock) {
|
|
263
|
+
addContentBlock(
|
|
264
|
+
<ColorizeCode
|
|
265
|
+
key="line-eof"
|
|
266
|
+
code={codeBlockContent.join('\n')}
|
|
267
|
+
language={codeBlockLang}
|
|
268
|
+
showLineNumbers={false}
|
|
269
|
+
/>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 处理未闭合的表格
|
|
274
|
+
if (inTable && tableHeaders.length > 0 && tableRows.length > 0) {
|
|
275
|
+
addContentBlock(
|
|
276
|
+
<TableRenderer
|
|
277
|
+
key={`table-${contentBlocks.length}`}
|
|
278
|
+
headers={tableHeaders}
|
|
279
|
+
rows={tableRows}
|
|
280
|
+
terminalWidth={terminalWidth}
|
|
281
|
+
/>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return <Box flexDirection="column">{contentBlocks}</Box>
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const MarkdownDisplay = React.memo(MarkdownDisplayInternal)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import Spinner from 'ink-spinner'
|
|
4
|
+
import { generateMultiStepCommand, type CommandStep, type ExecutedStep } from '../multi-step.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 MultiStepCommandGeneratorProps {
|
|
12
|
+
prompt: string
|
|
13
|
+
debug?: boolean
|
|
14
|
+
onStepComplete: (step: {
|
|
15
|
+
command: string
|
|
16
|
+
confirmed: boolean
|
|
17
|
+
cancelled?: boolean
|
|
18
|
+
hasBuiltin?: boolean
|
|
19
|
+
builtins?: string[]
|
|
20
|
+
reasoning?: string
|
|
21
|
+
needsContinue?: boolean
|
|
22
|
+
nextStepHint?: string
|
|
23
|
+
debugInfo?: any
|
|
24
|
+
}) => void
|
|
25
|
+
previousSteps?: ExecutedStep[]
|
|
26
|
+
currentStepNumber?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type State =
|
|
30
|
+
| { type: 'thinking' }
|
|
31
|
+
| { type: 'showing_command'; stepData: CommandStep }
|
|
32
|
+
| { type: 'cancelled'; command: string }
|
|
33
|
+
| { type: 'error'; error: string }
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* MultiStepCommandGenerator 组件 - 多步骤命令生成
|
|
37
|
+
* 每次只生成一个命令,支持 continue 机制
|
|
38
|
+
*/
|
|
39
|
+
export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps> = ({
|
|
40
|
+
prompt,
|
|
41
|
+
debug,
|
|
42
|
+
previousSteps = [],
|
|
43
|
+
currentStepNumber = 1,
|
|
44
|
+
onStepComplete,
|
|
45
|
+
}) => {
|
|
46
|
+
const [state, setState] = useState<State>({ type: 'thinking' })
|
|
47
|
+
const [thinkDuration, setThinkDuration] = useState(0)
|
|
48
|
+
const [debugInfo, setDebugInfo] = useState<any>(null)
|
|
49
|
+
|
|
50
|
+
// 初始化:调用 Mastra 生成命令
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const thinkStart = Date.now()
|
|
53
|
+
|
|
54
|
+
generateMultiStepCommand(prompt, previousSteps, { debug })
|
|
55
|
+
.then((result) => {
|
|
56
|
+
const thinkEnd = Date.now()
|
|
57
|
+
setThinkDuration(thinkEnd - thinkStart)
|
|
58
|
+
|
|
59
|
+
// 保存调试信息
|
|
60
|
+
if (debug && result.debugInfo) {
|
|
61
|
+
setDebugInfo(result.debugInfo)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setState({
|
|
65
|
+
type: 'showing_command',
|
|
66
|
+
stepData: result.stepData,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// 检测 builtin
|
|
70
|
+
const { hasBuiltin, builtins } = detectBuiltin(result.stepData.command)
|
|
71
|
+
|
|
72
|
+
if (hasBuiltin) {
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
onStepComplete({
|
|
75
|
+
command: result.stepData.command,
|
|
76
|
+
confirmed: false,
|
|
77
|
+
hasBuiltin: true,
|
|
78
|
+
builtins,
|
|
79
|
+
reasoning: result.stepData.reasoning,
|
|
80
|
+
needsContinue: result.stepData.continue,
|
|
81
|
+
})
|
|
82
|
+
}, 100)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
.catch((error: any) => {
|
|
86
|
+
setState({ type: 'error', error: error.message })
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
onStepComplete({
|
|
89
|
+
command: '',
|
|
90
|
+
confirmed: false,
|
|
91
|
+
cancelled: true,
|
|
92
|
+
})
|
|
93
|
+
}, 100)
|
|
94
|
+
})
|
|
95
|
+
}, [prompt, previousSteps, debug])
|
|
96
|
+
|
|
97
|
+
// 处理确认
|
|
98
|
+
const handleConfirm = () => {
|
|
99
|
+
if (state.type === 'showing_command') {
|
|
100
|
+
onStepComplete({
|
|
101
|
+
command: state.stepData.command,
|
|
102
|
+
confirmed: true,
|
|
103
|
+
reasoning: state.stepData.reasoning,
|
|
104
|
+
needsContinue: state.stepData.continue,
|
|
105
|
+
nextStepHint: state.stepData.nextStepHint,
|
|
106
|
+
debugInfo: debugInfo,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 处理取消
|
|
112
|
+
const handleCancel = () => {
|
|
113
|
+
if (state.type === 'showing_command') {
|
|
114
|
+
setState({ type: 'cancelled', command: state.stepData.command })
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
onStepComplete({
|
|
117
|
+
command: state.stepData.command,
|
|
118
|
+
confirmed: false,
|
|
119
|
+
cancelled: true,
|
|
120
|
+
})
|
|
121
|
+
}, 100)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Box flexDirection="column">
|
|
127
|
+
{/* 思考阶段 */}
|
|
128
|
+
{state.type === 'thinking' && (
|
|
129
|
+
<Box>
|
|
130
|
+
<Text color={theme.info}>
|
|
131
|
+
<Spinner type="dots" />{' '}
|
|
132
|
+
{currentStepNumber === 1 ? '正在思考...' : `正在规划步骤 ${currentStepNumber}...`}
|
|
133
|
+
</Text>
|
|
134
|
+
</Box>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{/* 思考完成 */}
|
|
138
|
+
{state.type !== 'thinking' && thinkDuration > 0 && (
|
|
139
|
+
<Box>
|
|
140
|
+
<Text color={theme.success}>✓ 思考完成 </Text>
|
|
141
|
+
<Duration ms={thinkDuration} />
|
|
142
|
+
</Box>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
{/* 显示步骤信息和命令 */}
|
|
146
|
+
{state.type === 'showing_command' && (
|
|
147
|
+
<>
|
|
148
|
+
{/* 调试信息 */}
|
|
149
|
+
{debug && debugInfo && (
|
|
150
|
+
<Box flexDirection="column" marginTop={1}>
|
|
151
|
+
<Text color={theme.accent}>━━━ 调试信息 ━━━</Text>
|
|
152
|
+
|
|
153
|
+
<Text color={theme.text.secondary}>完整系统提示词:</Text>
|
|
154
|
+
<Text color={theme.text.dim}>{debugInfo.fullPrompt}</Text>
|
|
155
|
+
|
|
156
|
+
<Box marginTop={1}>
|
|
157
|
+
<Text color={theme.text.secondary}>用户 Prompt: {debugInfo.userPrompt}</Text>
|
|
158
|
+
</Box>
|
|
159
|
+
|
|
160
|
+
{debugInfo.previousStepsCount > 0 && (
|
|
161
|
+
<Box marginTop={1}>
|
|
162
|
+
<Text color={theme.text.secondary}>已执行步骤数: {debugInfo.previousStepsCount}</Text>
|
|
163
|
+
</Box>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
<Box marginTop={1}>
|
|
167
|
+
<Text color={theme.text.secondary}>AI 返回的 JSON:</Text>
|
|
168
|
+
</Box>
|
|
169
|
+
<Text color={theme.text.dim}>{JSON.stringify(debugInfo.response, null, 2)}</Text>
|
|
170
|
+
|
|
171
|
+
<Text color={theme.accent}>━━━━━━━━━━━━━━━━</Text>
|
|
172
|
+
</Box>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{/* 步骤信息(仅多步骤时显示) */}
|
|
176
|
+
{state.stepData.continue === true && (
|
|
177
|
+
<Box flexDirection="column" marginTop={1}>
|
|
178
|
+
<Text color={theme.text.secondary}>步骤 {currentStepNumber}/?</Text>
|
|
179
|
+
{state.stepData.reasoning && (
|
|
180
|
+
<Text color={theme.text.muted}>原因: {state.stepData.reasoning}</Text>
|
|
181
|
+
)}
|
|
182
|
+
{state.stepData.nextStepHint && (
|
|
183
|
+
<Text color={theme.text.muted}>下一步: {state.stepData.nextStepHint}</Text>
|
|
184
|
+
)}
|
|
185
|
+
</Box>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
{/* 命令框 */}
|
|
189
|
+
<CommandBox command={state.stepData.command} />
|
|
190
|
+
|
|
191
|
+
{/* Builtin 警告 */}
|
|
192
|
+
{(() => {
|
|
193
|
+
const { hasBuiltin, builtins } = detectBuiltin(state.stepData.command)
|
|
194
|
+
if (hasBuiltin) {
|
|
195
|
+
return (
|
|
196
|
+
<Box flexDirection="column" marginY={1}>
|
|
197
|
+
<Text color={theme.error}>
|
|
198
|
+
⚠️ 此命令包含 shell 内置命令({formatBuiltins(builtins)}),无法在子进程中生效
|
|
199
|
+
</Text>
|
|
200
|
+
<Text color={theme.warning}>💡 请手动复制到终端执行</Text>
|
|
201
|
+
</Box>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
return null
|
|
205
|
+
})()}
|
|
206
|
+
|
|
207
|
+
{/* 确认提示 */}
|
|
208
|
+
{!detectBuiltin(state.stepData.command).hasBuiltin && (
|
|
209
|
+
<ConfirmationPrompt prompt="执行?" onConfirm={handleConfirm} onCancel={handleCancel} />
|
|
210
|
+
)}
|
|
211
|
+
</>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{/* 取消 */}
|
|
215
|
+
{state.type === 'cancelled' && (
|
|
216
|
+
<Box marginTop={1}>
|
|
217
|
+
<Text color={theme.text.secondary}>已取消执行</Text>
|
|
218
|
+
</Box>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{/* 错误 */}
|
|
222
|
+
{state.type === 'error' && (
|
|
223
|
+
<Box marginTop={1}>
|
|
224
|
+
<Text color={theme.error}>❌ 错误: {state.error}</Text>
|
|
225
|
+
</Box>
|
|
226
|
+
)}
|
|
227
|
+
</Box>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import stringWidth from 'string-width'
|
|
4
|
+
import { theme } from '../ui/theme.js'
|
|
5
|
+
import { RenderInline } from './InlineRenderer.js'
|
|
6
|
+
|
|
7
|
+
interface TableRendererProps {
|
|
8
|
+
headers: string[]
|
|
9
|
+
rows: string[][]
|
|
10
|
+
terminalWidth: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 计算纯文本长度(去除 markdown 标记)
|
|
15
|
+
*/
|
|
16
|
+
function getPlainTextLength(text: string): number {
|
|
17
|
+
const cleanText = text
|
|
18
|
+
.replace(/\*\*(.*?)\*\*/g, '$1')
|
|
19
|
+
.replace(/\*(.*?)\*/g, '$1')
|
|
20
|
+
.replace(/_(.*?)_/g, '$1')
|
|
21
|
+
.replace(/~~(.*?)~~/g, '$1')
|
|
22
|
+
.replace(/`(.*?)`/g, '$1')
|
|
23
|
+
.replace(/<u>(.*?)<\/u>/g, '$1')
|
|
24
|
+
.replace(/\[(.*?)\]\(.*?\)/g, '$1')
|
|
25
|
+
return stringWidth(cleanText)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 计算每列的最佳宽度
|
|
30
|
+
*/
|
|
31
|
+
function calculateColumnWidths(
|
|
32
|
+
headers: string[],
|
|
33
|
+
rows: string[][],
|
|
34
|
+
terminalWidth: number
|
|
35
|
+
): number[] {
|
|
36
|
+
const columnCount = headers.length
|
|
37
|
+
const minWidths = headers.map((h, i) => {
|
|
38
|
+
const headerWidth = getPlainTextLength(h)
|
|
39
|
+
const maxRowWidth = Math.max(...rows.map(row => getPlainTextLength(row[i] || '')))
|
|
40
|
+
return Math.max(headerWidth, maxRowWidth, 3) // 最小宽度 3
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const totalMinWidth = minWidths.reduce((a, b) => a + b, 0) + (columnCount + 1) * 3 // 加边框和间距
|
|
44
|
+
|
|
45
|
+
if (totalMinWidth <= terminalWidth) {
|
|
46
|
+
return minWidths
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 如果超宽,平均分配
|
|
50
|
+
const availableWidth = terminalWidth - (columnCount + 1) * 3
|
|
51
|
+
const avgWidth = Math.floor(availableWidth / columnCount)
|
|
52
|
+
return headers.map(() => Math.max(avgWidth, 5))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 表格渲染组件
|
|
57
|
+
*/
|
|
58
|
+
function TableRendererInternal({ headers, rows, terminalWidth }: TableRendererProps) {
|
|
59
|
+
const columnWidths = calculateColumnWidths(headers, rows, terminalWidth)
|
|
60
|
+
const baseColor = theme.text.primary
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Box flexDirection="column" marginY={1}>
|
|
64
|
+
{/* 表头 */}
|
|
65
|
+
<Box>
|
|
66
|
+
<Text color={theme.border}>│ </Text>
|
|
67
|
+
{headers.map((header, i) => (
|
|
68
|
+
<React.Fragment key={i}>
|
|
69
|
+
<Box width={columnWidths[i]}>
|
|
70
|
+
<Text bold color={theme.primary}>
|
|
71
|
+
<RenderInline text={header} defaultColor={theme.primary} />
|
|
72
|
+
</Text>
|
|
73
|
+
</Box>
|
|
74
|
+
<Text color={theme.border}> │ </Text>
|
|
75
|
+
</React.Fragment>
|
|
76
|
+
))}
|
|
77
|
+
</Box>
|
|
78
|
+
|
|
79
|
+
{/* 分隔线 */}
|
|
80
|
+
<Box>
|
|
81
|
+
<Text color={theme.border}>├─</Text>
|
|
82
|
+
{columnWidths.map((width, i) => (
|
|
83
|
+
<React.Fragment key={i}>
|
|
84
|
+
<Text color={theme.border}>{'─'.repeat(width)}</Text>
|
|
85
|
+
<Text color={theme.border}>─┼─</Text>
|
|
86
|
+
</React.Fragment>
|
|
87
|
+
))}
|
|
88
|
+
</Box>
|
|
89
|
+
|
|
90
|
+
{/* 表格行 */}
|
|
91
|
+
{rows.map((row, rowIndex) => (
|
|
92
|
+
<Box key={rowIndex}>
|
|
93
|
+
<Text color={theme.border}>│ </Text>
|
|
94
|
+
{row.map((cell, cellIndex) => (
|
|
95
|
+
<React.Fragment key={cellIndex}>
|
|
96
|
+
<Box width={columnWidths[cellIndex]}>
|
|
97
|
+
<Text color={baseColor}>
|
|
98
|
+
<RenderInline text={cell || ''} defaultColor={baseColor} />
|
|
99
|
+
</Text>
|
|
100
|
+
</Box>
|
|
101
|
+
<Text color={theme.border}> │ </Text>
|
|
102
|
+
</React.Fragment>
|
|
103
|
+
))}
|
|
104
|
+
</Box>
|
|
105
|
+
))}
|
|
106
|
+
</Box>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const TableRenderer = React.memo(TableRendererInternal)
|