@yivan-lab/pretty-please 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +283 -1
  2. package/bin/pls.tsx +1022 -104
  3. package/dist/bin/pls.js +894 -84
  4. package/dist/package.json +4 -4
  5. package/dist/src/alias.d.ts +41 -0
  6. package/dist/src/alias.js +240 -0
  7. package/dist/src/chat-history.js +10 -1
  8. package/dist/src/components/Chat.js +2 -1
  9. package/dist/src/components/CodeColorizer.js +26 -20
  10. package/dist/src/components/CommandBox.js +2 -1
  11. package/dist/src/components/ConfirmationPrompt.js +2 -1
  12. package/dist/src/components/Duration.js +2 -1
  13. package/dist/src/components/InlineRenderer.js +2 -1
  14. package/dist/src/components/MarkdownDisplay.js +2 -1
  15. package/dist/src/components/MultiStepCommandGenerator.d.ts +3 -1
  16. package/dist/src/components/MultiStepCommandGenerator.js +20 -10
  17. package/dist/src/components/TableRenderer.js +2 -1
  18. package/dist/src/config.d.ts +34 -3
  19. package/dist/src/config.js +71 -31
  20. package/dist/src/multi-step.d.ts +22 -6
  21. package/dist/src/multi-step.js +27 -4
  22. package/dist/src/remote-history.d.ts +63 -0
  23. package/dist/src/remote-history.js +315 -0
  24. package/dist/src/remote.d.ts +113 -0
  25. package/dist/src/remote.js +634 -0
  26. package/dist/src/shell-hook.d.ts +53 -0
  27. package/dist/src/shell-hook.js +242 -19
  28. package/dist/src/ui/theme.d.ts +27 -24
  29. package/dist/src/ui/theme.js +71 -21
  30. package/dist/src/upgrade.d.ts +41 -0
  31. package/dist/src/upgrade.js +348 -0
  32. package/dist/src/utils/console.js +22 -11
  33. package/package.json +4 -4
  34. package/src/alias.ts +301 -0
  35. package/src/chat-history.ts +11 -1
  36. package/src/components/Chat.tsx +2 -1
  37. package/src/components/CodeColorizer.tsx +27 -19
  38. package/src/components/CommandBox.tsx +2 -1
  39. package/src/components/ConfirmationPrompt.tsx +2 -1
  40. package/src/components/Duration.tsx +2 -1
  41. package/src/components/InlineRenderer.tsx +2 -1
  42. package/src/components/MarkdownDisplay.tsx +2 -1
  43. package/src/components/MultiStepCommandGenerator.tsx +25 -11
  44. package/src/components/TableRenderer.tsx +2 -1
  45. package/src/config.ts +117 -32
  46. package/src/multi-step.ts +43 -6
  47. package/src/remote-history.ts +390 -0
  48. package/src/remote.ts +800 -0
  49. package/src/shell-hook.ts +271 -19
  50. package/src/ui/theme.ts +101 -24
  51. package/src/upgrade.ts +397 -0
  52. package/src/utils/console.ts +22 -11
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
+ }
@@ -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.cyan(num.toString().padStart(2, ' '))}. ${msg.content}`)
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)))
@@ -4,7 +4,7 @@ import Spinner from 'ink-spinner'
4
4
  import { MarkdownDisplay } from './MarkdownDisplay.js'
5
5
  import { chatWithMastra } from '../mastra-chat.js'
6
6
  import { getChatRoundCount } from '../chat-history.js'
7
- import { theme } from '../ui/theme.js'
7
+ import { getCurrentTheme } from '../ui/theme.js'
8
8
 
9
9
  interface ChatProps {
10
10
  prompt: string
@@ -28,6 +28,7 @@ interface DebugInfo {
28
28
  * 使用正常渲染,完成后保持最后一帧在终端
29
29
  */
30
30
  export function Chat({ prompt, debug, showRoundCount, onComplete }: ChatProps) {
31
+ const theme = getCurrentTheme()
31
32
  const [status, setStatus] = useState<Status>('thinking')
32
33
  const [content, setContent] = useState('')
33
34
  const [duration, setDuration] = useState(0)
@@ -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 { theme } from '../ui/theme.js'
5
+ import { getCurrentTheme, type Theme } from '../ui/theme.js'
6
6
 
7
7
  // 创建 lowlight 实例
8
8
  const lowlight = createLowlight(common)
9
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,
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,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import { Box, Text } from 'ink'
3
- import { theme } from '../ui/theme.js'
3
+ import { getCurrentTheme } from '../ui/theme.js'
4
4
  import { getDisplayWidth } from '../utils/console.js'
5
5
 
6
6
  interface CommandBoxProps {
@@ -12,6 +12,7 @@ interface CommandBoxProps {
12
12
  * CommandBox 组件 - 显示带边框和标题的命令框
13
13
  */
14
14
  export const CommandBox: React.FC<CommandBoxProps> = ({ command, title = '生成命令' }) => {
15
+ const theme = getCurrentTheme()
15
16
  const lines = command.split('\n')
16
17
  const titleWidth = getDisplayWidth(title)
17
18
  const maxContentWidth = Math.max(...lines.map(l => getDisplayWidth(l)))
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import { Text, useInput } from 'ink'
3
- import { theme } from '../ui/theme.js'
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 { theme } from '../ui/theme.js'
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 { theme } from '../ui/theme.js'
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
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import { Text, Box } from 'ink'
3
- import { theme } from '../ui/theme.js'
3
+ import { getCurrentTheme } from '../ui/theme.js'
4
4
  import { ColorizeCode } from './CodeColorizer.js'
5
5
  import { TableRenderer } from './TableRenderer.js'
6
6
  import { RenderInline } from './InlineRenderer.js'
@@ -18,6 +18,7 @@ interface MarkdownDisplayProps {
18
18
  function MarkdownDisplayInternal({ text, terminalWidth = 80 }: MarkdownDisplayProps) {
19
19
  if (!text) return <></>
20
20
 
21
+ const theme = getCurrentTheme()
21
22
  const lines = text.split(/\r?\n/)
22
23
 
23
24
  // 正则表达式
@@ -2,12 +2,12 @@ import React, { useState, useEffect } from 'react'
2
2
  import { Box, Text, useInput } from 'ink'
3
3
  import TextInput from 'ink-text-input'
4
4
  import Spinner from 'ink-spinner'
5
- import { generateMultiStepCommand, type CommandStep, type ExecutedStep } from '../multi-step.js'
5
+ import { generateMultiStepCommand, type CommandStep, type ExecutedStep, type RemoteContext } from '../multi-step.js'
6
6
  import { detectBuiltin, formatBuiltins } from '../builtin-detector.js'
7
7
  import { CommandBox } from './CommandBox.js'
8
8
  import { ConfirmationPrompt } from './ConfirmationPrompt.js'
9
9
  import { Duration } from './Duration.js'
10
- import { theme } from '../ui/theme.js'
10
+ import { getCurrentTheme } from '../ui/theme.js'
11
11
  import { getConfig } from '../config.js'
12
12
 
13
13
  interface MultiStepCommandGeneratorProps {
@@ -28,6 +28,8 @@ interface MultiStepCommandGeneratorProps {
28
28
  }) => void
29
29
  previousSteps?: ExecutedStep[]
30
30
  currentStepNumber?: number
31
+ remoteContext?: RemoteContext // 远程执行上下文
32
+ isRemote?: boolean // 是否为远程执行(远程执行时不检测 builtin)
31
33
  }
32
34
 
33
35
  type State =
@@ -46,8 +48,11 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
46
48
  debug,
47
49
  previousSteps = [],
48
50
  currentStepNumber = 1,
51
+ remoteContext,
52
+ isRemote = false,
49
53
  onStepComplete,
50
54
  }) => {
55
+ const theme = getCurrentTheme()
51
56
  const [state, setState] = useState<State>({ type: 'thinking' })
52
57
  const [thinkDuration, setThinkDuration] = useState(0)
53
58
  const [debugInfo, setDebugInfo] = useState<any>(null)
@@ -67,7 +72,7 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
67
72
  useEffect(() => {
68
73
  const thinkStart = Date.now()
69
74
 
70
- generateMultiStepCommand(prompt, previousSteps, { debug })
75
+ generateMultiStepCommand(prompt, previousSteps, { debug, remoteContext })
71
76
  .then((result) => {
72
77
  const thinkEnd = Date.now()
73
78
  setThinkDuration(thinkEnd - thinkStart)
@@ -91,11 +96,11 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
91
96
  return
92
97
  }
93
98
 
94
- // 检测 builtin(优先检测)
99
+ // 检测 builtin(优先检测,但远程执行时跳过)
95
100
  const { hasBuiltin, builtins } = detectBuiltin(result.stepData.command)
96
101
 
97
- if (hasBuiltin) {
98
- // 有 builtin,不管什么模式都不编辑,直接提示
102
+ if (hasBuiltin && !isRemote) {
103
+ // 有 builtin 且是本地执行,不管什么模式都不编辑,直接提示
99
104
  setState({
100
105
  type: 'showing_command',
101
106
  stepData: result.stepData,
@@ -142,7 +147,7 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
142
147
  })
143
148
  }, 100)
144
149
  })
145
- }, [prompt, previousSteps, debug])
150
+ }, [prompt, previousSteps, debug, remoteContext])
146
151
 
147
152
  // 处理确认
148
153
  const handleConfirm = () => {
@@ -228,7 +233,10 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
228
233
  <Box>
229
234
  <Text color={theme.info}>
230
235
  <Spinner type="dots" />{' '}
231
- {currentStepNumber === 1 ? '正在思考...' : `正在规划步骤 ${currentStepNumber}...`}
236
+ {remoteContext
237
+ ? (currentStepNumber === 1 ? `正在为 ${remoteContext.name} 思考...` : `正在规划步骤 ${currentStepNumber} (${remoteContext.name})...`)
238
+ : (currentStepNumber === 1 ? '正在思考...' : `正在规划步骤 ${currentStepNumber}...`)
239
+ }
232
240
  </Text>
233
241
  </Box>
234
242
  )}
@@ -262,6 +270,12 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
262
270
  </Box>
263
271
  )}
264
272
 
273
+ {debugInfo.remoteContext && (
274
+ <Box marginTop={1}>
275
+ <Text color={theme.text.secondary}>远程服务器: {debugInfo.remoteContext.name} ({debugInfo.remoteContext.sysInfo.os})</Text>
276
+ </Box>
277
+ )}
278
+
265
279
  <Box marginTop={1}>
266
280
  <Text color={theme.text.secondary}>AI 返回的 JSON:</Text>
267
281
  </Box>
@@ -287,8 +301,8 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
287
301
  {/* 命令框 */}
288
302
  <CommandBox command={state.stepData.command} />
289
303
 
290
- {/* Builtin 警告 */}
291
- {(() => {
304
+ {/* Builtin 警告(仅本地执行时显示) */}
305
+ {!isRemote && (() => {
292
306
  const { hasBuiltin, builtins } = detectBuiltin(state.stepData.command)
293
307
  if (hasBuiltin) {
294
308
  return (
@@ -304,7 +318,7 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
304
318
  })()}
305
319
 
306
320
  {/* 确认提示 */}
307
- {!detectBuiltin(state.stepData.command).hasBuiltin && (
321
+ {(isRemote || !detectBuiltin(state.stepData.command).hasBuiltin) && (
308
322
  <ConfirmationPrompt
309
323
  prompt="执行?"
310
324
  onConfirm={handleConfirm}
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { Box, Text } from 'ink'
3
3
  import stringWidth from 'string-width'
4
- import { theme } from '../ui/theme.js'
4
+ import { getCurrentTheme } from '../ui/theme.js'
5
5
  import { RenderInline } from './InlineRenderer.js'
6
6
 
7
7
  interface TableRendererProps {
@@ -56,6 +56,7 @@ function calculateColumnWidths(
56
56
  * 表格渲染组件
57
57
  */
58
58
  function TableRendererInternal({ headers, rows, terminalWidth }: TableRendererProps) {
59
+ const theme = getCurrentTheme()
59
60
  const columnWidths = calculateColumnWidths(headers, rows, terminalWidth)
60
61
  const baseColor = theme.text.primary
61
62