@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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +380 -0
  3. package/bin/pls.js +681 -0
  4. package/bin/pls.tsx +541 -0
  5. package/dist/bin/pls.d.ts +2 -0
  6. package/dist/bin/pls.js +429 -0
  7. package/dist/src/ai.d.ts +48 -0
  8. package/dist/src/ai.js +295 -0
  9. package/dist/src/builtin-detector.d.ts +15 -0
  10. package/dist/src/builtin-detector.js +83 -0
  11. package/dist/src/chat-history.d.ts +26 -0
  12. package/dist/src/chat-history.js +81 -0
  13. package/dist/src/components/Chat.d.ts +13 -0
  14. package/dist/src/components/Chat.js +80 -0
  15. package/dist/src/components/ChatStatus.d.ts +9 -0
  16. package/dist/src/components/ChatStatus.js +34 -0
  17. package/dist/src/components/CodeColorizer.d.ts +12 -0
  18. package/dist/src/components/CodeColorizer.js +82 -0
  19. package/dist/src/components/CommandBox.d.ts +10 -0
  20. package/dist/src/components/CommandBox.js +45 -0
  21. package/dist/src/components/CommandGenerator.d.ts +20 -0
  22. package/dist/src/components/CommandGenerator.js +116 -0
  23. package/dist/src/components/ConfigDisplay.d.ts +9 -0
  24. package/dist/src/components/ConfigDisplay.js +42 -0
  25. package/dist/src/components/ConfigWizard.d.ts +9 -0
  26. package/dist/src/components/ConfigWizard.js +72 -0
  27. package/dist/src/components/ConfirmationPrompt.d.ts +12 -0
  28. package/dist/src/components/ConfirmationPrompt.js +26 -0
  29. package/dist/src/components/Duration.d.ts +9 -0
  30. package/dist/src/components/Duration.js +21 -0
  31. package/dist/src/components/HistoryDisplay.d.ts +9 -0
  32. package/dist/src/components/HistoryDisplay.js +51 -0
  33. package/dist/src/components/HookManager.d.ts +10 -0
  34. package/dist/src/components/HookManager.js +88 -0
  35. package/dist/src/components/InlineRenderer.d.ts +12 -0
  36. package/dist/src/components/InlineRenderer.js +75 -0
  37. package/dist/src/components/MarkdownDisplay.d.ts +13 -0
  38. package/dist/src/components/MarkdownDisplay.js +197 -0
  39. package/dist/src/components/MultiStepCommandGenerator.d.ts +25 -0
  40. package/dist/src/components/MultiStepCommandGenerator.js +142 -0
  41. package/dist/src/components/TableRenderer.d.ts +12 -0
  42. package/dist/src/components/TableRenderer.js +66 -0
  43. package/dist/src/config.d.ts +29 -0
  44. package/dist/src/config.js +203 -0
  45. package/dist/src/history.d.ts +20 -0
  46. package/dist/src/history.js +113 -0
  47. package/dist/src/mastra-agent.d.ts +7 -0
  48. package/dist/src/mastra-agent.js +31 -0
  49. package/dist/src/multi-step.d.ts +41 -0
  50. package/dist/src/multi-step.js +67 -0
  51. package/dist/src/shell-hook.d.ts +35 -0
  52. package/dist/src/shell-hook.js +348 -0
  53. package/dist/src/sysinfo.d.ts +15 -0
  54. package/dist/src/sysinfo.js +52 -0
  55. package/dist/src/ui/theme.d.ts +26 -0
  56. package/dist/src/ui/theme.js +31 -0
  57. package/dist/src/utils/console.d.ts +44 -0
  58. package/dist/src/utils/console.js +114 -0
  59. package/package.json +78 -0
  60. package/src/ai.js +324 -0
  61. package/src/builtin-detector.js +98 -0
  62. package/src/chat-history.js +94 -0
  63. package/src/components/Chat.tsx +122 -0
  64. package/src/components/ChatStatus.tsx +53 -0
  65. package/src/components/CodeColorizer.tsx +128 -0
  66. package/src/components/CommandBox.tsx +60 -0
  67. package/src/components/CommandGenerator.tsx +184 -0
  68. package/src/components/ConfigDisplay.tsx +64 -0
  69. package/src/components/ConfigWizard.tsx +101 -0
  70. package/src/components/ConfirmationPrompt.tsx +41 -0
  71. package/src/components/Duration.tsx +24 -0
  72. package/src/components/HistoryDisplay.tsx +69 -0
  73. package/src/components/HookManager.tsx +150 -0
  74. package/src/components/InlineRenderer.tsx +123 -0
  75. package/src/components/MarkdownDisplay.tsx +288 -0
  76. package/src/components/MultiStepCommandGenerator.tsx +229 -0
  77. package/src/components/TableRenderer.tsx +110 -0
  78. package/src/config.js +221 -0
  79. package/src/history.js +131 -0
  80. package/src/mastra-agent.ts +35 -0
  81. package/src/multi-step.ts +93 -0
  82. package/src/shell-hook.js +393 -0
  83. package/src/sysinfo.js +57 -0
  84. package/src/ui/theme.ts +37 -0
  85. package/src/utils/console.js +130 -0
  86. 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)