@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/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yivan-lab/pretty-please",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "AI 驱动的命令行工具,将自然语言转换为可执行的 Shell 命令",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,12 +31,12 @@
31
31
  "license": "MIT",
32
32
  "repository": {
33
33
  "type": "git",
34
- "url": "https://github.com/yivan-lab/pretty-please.git"
34
+ "url": "https://github.com/IvanLark/pretty-please.git"
35
35
  },
36
36
  "bugs": {
37
- "url": "https://github.com/yivan-lab/pretty-please/issues"
37
+ "url": "https://github.com/IvanLark/pretty-please/issues"
38
38
  },
39
- "homepage": "https://github.com/yivan-lab/pretty-please#readme",
39
+ "homepage": "https://github.com/IvanLark/pretty-please#readme",
40
40
  "publishConfig": {
41
41
  "access": "public"
42
42
  },
@@ -0,0 +1,41 @@
1
+ import { type AliasConfig } from './config.js';
2
+ /**
3
+ * 别名解析结果
4
+ */
5
+ export interface AliasResolveResult {
6
+ resolved: boolean;
7
+ prompt: string;
8
+ aliasName?: string;
9
+ originalInput?: string;
10
+ }
11
+ /**
12
+ * 获取所有别名
13
+ */
14
+ export declare function getAliases(): Record<string, AliasConfig>;
15
+ /**
16
+ * 添加别名
17
+ * @param name 别名名称
18
+ * @param prompt 对应的 prompt
19
+ * @param description 可选描述
20
+ * @param reservedCommands 保留的子命令列表(动态传入)
21
+ */
22
+ export declare function addAlias(name: string, prompt: string, description?: string, reservedCommands?: string[]): void;
23
+ /**
24
+ * 删除别名
25
+ */
26
+ export declare function removeAlias(name: string): boolean;
27
+ /**
28
+ * 解析别名
29
+ * 支持 `pls disk` 和 `pls @disk` 两种格式
30
+ * @param input 用户输入(可能是别名或普通 prompt)
31
+ * @returns 解析结果
32
+ */
33
+ export declare function resolveAlias(input: string): AliasResolveResult;
34
+ /**
35
+ * 显示所有别名
36
+ */
37
+ export declare function displayAliases(): void;
38
+ /**
39
+ * 获取别名的参数信息(用于帮助显示)
40
+ */
41
+ export declare function getAliasParams(aliasName: string): string[];
@@ -0,0 +1,240 @@
1
+ import chalk from 'chalk';
2
+ import { getConfig, saveConfig } from './config.js';
3
+ import { getCurrentTheme } from './ui/theme.js';
4
+ // 获取主题颜色
5
+ function getColors() {
6
+ const theme = getCurrentTheme();
7
+ return {
8
+ primary: theme.primary,
9
+ secondary: theme.secondary,
10
+ success: theme.success,
11
+ error: theme.error,
12
+ warning: theme.warning,
13
+ muted: theme.text.muted,
14
+ };
15
+ }
16
+ /**
17
+ * 获取所有别名
18
+ */
19
+ export function getAliases() {
20
+ const config = getConfig();
21
+ return config.aliases || {};
22
+ }
23
+ /**
24
+ * 添加别名
25
+ * @param name 别名名称
26
+ * @param prompt 对应的 prompt
27
+ * @param description 可选描述
28
+ * @param reservedCommands 保留的子命令列表(动态传入)
29
+ */
30
+ export function addAlias(name, prompt, description, reservedCommands = []) {
31
+ // 验证别名名称
32
+ if (!name || !name.trim()) {
33
+ throw new Error('别名名称不能为空');
34
+ }
35
+ // 移除可能的 @ 前缀
36
+ const aliasName = name.startsWith('@') ? name.slice(1) : name;
37
+ // 验证别名名称格式(只允许字母、数字、下划线、连字符)
38
+ if (!/^[a-zA-Z0-9_-]+$/.test(aliasName)) {
39
+ throw new Error('别名名称只能包含字母、数字、下划线和连字符');
40
+ }
41
+ // 检查是否与保留命令冲突
42
+ if (reservedCommands.includes(aliasName)) {
43
+ throw new Error(`"${aliasName}" 是保留的子命令,不能用作别名`);
44
+ }
45
+ // 验证 prompt
46
+ if (!prompt || !prompt.trim()) {
47
+ throw new Error('prompt 不能为空');
48
+ }
49
+ const config = getConfig();
50
+ if (!config.aliases) {
51
+ config.aliases = {};
52
+ }
53
+ config.aliases[aliasName] = {
54
+ prompt: prompt.trim(),
55
+ description: description?.trim(),
56
+ };
57
+ saveConfig(config);
58
+ }
59
+ /**
60
+ * 删除别名
61
+ */
62
+ export function removeAlias(name) {
63
+ // 移除可能的 @ 前缀
64
+ const aliasName = name.startsWith('@') ? name.slice(1) : name;
65
+ const config = getConfig();
66
+ if (!config.aliases || !config.aliases[aliasName]) {
67
+ return false;
68
+ }
69
+ delete config.aliases[aliasName];
70
+ saveConfig(config);
71
+ return true;
72
+ }
73
+ /**
74
+ * 解析参数模板
75
+ * 支持格式:{{param}} 或 {{param:default}}
76
+ */
77
+ function parseTemplateParams(prompt) {
78
+ const regex = /\{\{([^}:]+)(?::[^}]*)?\}\}/g;
79
+ const params = [];
80
+ let match;
81
+ while ((match = regex.exec(prompt)) !== null) {
82
+ if (!params.includes(match[1])) {
83
+ params.push(match[1]);
84
+ }
85
+ }
86
+ return params;
87
+ }
88
+ /**
89
+ * 替换模板参数
90
+ * @param prompt 原始 prompt(可能包含模板参数)
91
+ * @param args 用户提供的参数(key=value 或 --key=value 格式)
92
+ */
93
+ function replaceTemplateParams(prompt, args) {
94
+ // 解析用户参数
95
+ const userParams = {};
96
+ for (const arg of args) {
97
+ // 支持 --key=value 或 key=value 格式
98
+ const cleanArg = arg.startsWith('--') ? arg.slice(2) : arg;
99
+ const eqIndex = cleanArg.indexOf('=');
100
+ if (eqIndex > 0) {
101
+ const key = cleanArg.slice(0, eqIndex);
102
+ const value = cleanArg.slice(eqIndex + 1);
103
+ userParams[key] = value;
104
+ }
105
+ }
106
+ // 替换模板参数
107
+ let result = prompt;
108
+ // 匹配 {{param}} 或 {{param:default}}
109
+ result = result.replace(/\{\{([^}:]+)(?::([^}]*))?\}\}/g, (match, param, defaultValue) => {
110
+ if (userParams[param] !== undefined) {
111
+ return userParams[param];
112
+ }
113
+ if (defaultValue !== undefined) {
114
+ return defaultValue;
115
+ }
116
+ // 没有提供值也没有默认值,保留原样(后面会报错或让用户补充)
117
+ return match;
118
+ });
119
+ return result;
120
+ }
121
+ /**
122
+ * 检查是否还有未替换的模板参数
123
+ */
124
+ function hasUnresolvedParams(prompt) {
125
+ const regex = /\{\{([^}:]+)\}\}/g;
126
+ const unresolved = [];
127
+ let match;
128
+ while ((match = regex.exec(prompt)) !== null) {
129
+ unresolved.push(match[1]);
130
+ }
131
+ return unresolved;
132
+ }
133
+ /**
134
+ * 解析别名
135
+ * 支持 `pls disk` 和 `pls @disk` 两种格式
136
+ * @param input 用户输入(可能是别名或普通 prompt)
137
+ * @returns 解析结果
138
+ */
139
+ export function resolveAlias(input) {
140
+ const parts = input.trim().split(/\s+/);
141
+ if (parts.length === 0) {
142
+ return { resolved: false, prompt: input };
143
+ }
144
+ let aliasName = parts[0];
145
+ const restArgs = parts.slice(1);
146
+ // 支持 @ 前缀
147
+ if (aliasName.startsWith('@')) {
148
+ aliasName = aliasName.slice(1);
149
+ }
150
+ const aliases = getAliases();
151
+ const aliasConfig = aliases[aliasName];
152
+ if (!aliasConfig) {
153
+ return { resolved: false, prompt: input };
154
+ }
155
+ // 检查是否有模板参数
156
+ const templateParams = parseTemplateParams(aliasConfig.prompt);
157
+ let resolvedPrompt;
158
+ if (templateParams.length > 0) {
159
+ // 有模板参数,进行替换
160
+ resolvedPrompt = replaceTemplateParams(aliasConfig.prompt, restArgs);
161
+ // 检查是否还有未替换的必填参数
162
+ const unresolved = hasUnresolvedParams(resolvedPrompt);
163
+ if (unresolved.length > 0) {
164
+ throw new Error(`别名 "${aliasName}" 缺少必填参数: ${unresolved.join(', ')}`);
165
+ }
166
+ // 过滤掉已用于参数替换的 args,剩余的追加到 prompt
167
+ const usedArgs = restArgs.filter((arg) => {
168
+ const cleanArg = arg.startsWith('--') ? arg.slice(2) : arg;
169
+ return cleanArg.includes('=');
170
+ });
171
+ const extraArgs = restArgs.filter((arg) => !usedArgs.includes(arg));
172
+ if (extraArgs.length > 0) {
173
+ resolvedPrompt = `${resolvedPrompt} ${extraArgs.join(' ')}`;
174
+ }
175
+ }
176
+ else {
177
+ // 没有模板参数,直接追加额外内容
178
+ if (restArgs.length > 0) {
179
+ resolvedPrompt = `${aliasConfig.prompt} ${restArgs.join(' ')}`;
180
+ }
181
+ else {
182
+ resolvedPrompt = aliasConfig.prompt;
183
+ }
184
+ }
185
+ return {
186
+ resolved: true,
187
+ prompt: resolvedPrompt,
188
+ aliasName,
189
+ originalInput: input,
190
+ };
191
+ }
192
+ /**
193
+ * 显示所有别名
194
+ */
195
+ export function displayAliases() {
196
+ const aliases = getAliases();
197
+ const colors = getColors();
198
+ const aliasNames = Object.keys(aliases);
199
+ console.log('');
200
+ if (aliasNames.length === 0) {
201
+ console.log(chalk.gray(' 暂无别名'));
202
+ console.log('');
203
+ console.log(chalk.gray(' 使用 pls alias add <name> "<prompt>" 添加别名'));
204
+ console.log('');
205
+ return;
206
+ }
207
+ console.log(chalk.bold('命令别名:'));
208
+ console.log(chalk.gray('━'.repeat(50)));
209
+ for (const name of aliasNames) {
210
+ const alias = aliases[name];
211
+ const params = parseTemplateParams(alias.prompt);
212
+ // 别名名称
213
+ let line = ` ${chalk.hex(colors.primary)(name)}`;
214
+ // 如果有参数,显示参数
215
+ if (params.length > 0) {
216
+ line += chalk.gray(` <${params.join('> <')}>`);
217
+ }
218
+ console.log(line);
219
+ // prompt 内容
220
+ console.log(` ${chalk.gray('→')} ${alias.prompt}`);
221
+ // 描述
222
+ if (alias.description) {
223
+ console.log(` ${chalk.gray(alias.description)}`);
224
+ }
225
+ console.log('');
226
+ }
227
+ console.log(chalk.gray('━'.repeat(50)));
228
+ console.log(chalk.gray('使用: pls <alias> 或 pls @<alias>'));
229
+ console.log('');
230
+ }
231
+ /**
232
+ * 获取别名的参数信息(用于帮助显示)
233
+ */
234
+ export function getAliasParams(aliasName) {
235
+ const aliases = getAliases();
236
+ const alias = aliases[aliasName];
237
+ if (!alias)
238
+ return [];
239
+ return parseTemplateParams(alias.prompt);
240
+ }
@@ -3,6 +3,14 @@ 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
+ function getColors() {
9
+ const theme = getCurrentTheme();
10
+ return {
11
+ primary: theme.primary,
12
+ };
13
+ }
6
14
  const CONFIG_DIR = path.join(os.homedir(), '.please');
7
15
  const CHAT_HISTORY_FILE = path.join(CONFIG_DIR, 'chat_history.json');
8
16
  /**
@@ -82,6 +90,7 @@ export function getChatRoundCount() {
82
90
  export function displayChatHistory() {
83
91
  const history = getChatHistory();
84
92
  const config = getConfig();
93
+ const colors = getColors();
85
94
  if (history.length === 0) {
86
95
  console.log('\n' + chalk.gray('暂无对话历史'));
87
96
  console.log('');
@@ -94,7 +103,7 @@ export function displayChatHistory() {
94
103
  console.log(chalk.gray('━'.repeat(50)));
95
104
  userMessages.forEach((msg, index) => {
96
105
  const num = index + 1;
97
- console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${msg.content}`);
106
+ console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${msg.content}`);
98
107
  });
99
108
  console.log(chalk.gray('━'.repeat(50)));
100
109
  console.log(chalk.gray(`配置: 保留最近 ${config.chatHistoryLimit} 轮对话`));
@@ -4,12 +4,13 @@ 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
  * Chat 组件 - AI 对话模式
10
10
  * 使用正常渲染,完成后保持最后一帧在终端
11
11
  */
12
12
  export function Chat({ prompt, debug, showRoundCount, onComplete }) {
13
+ const theme = getCurrentTheme();
13
14
  const [status, setStatus] = useState('thinking');
14
15
  const [content, setContent] = useState('');
15
16
  const [duration, setDuration] = useState(0);
@@ -1,26 +1,30 @@
1
1
  import React from 'react';
2
2
  import { Text, Box } from 'ink';
3
3
  import { common, createLowlight } from 'lowlight';
4
- import { theme } from '../ui/theme.js';
4
+ import { getCurrentTheme } from '../ui/theme.js';
5
5
  // 创建 lowlight 实例
6
6
  const lowlight = createLowlight(common);
7
- // 语法高亮颜色映射
8
- const syntaxColors = {
9
- 'hljs-keyword': theme.code.keyword,
10
- 'hljs-string': theme.code.string,
11
- 'hljs-function': theme.code.function,
12
- 'hljs-comment': theme.code.comment,
13
- 'hljs-number': theme.primary,
14
- 'hljs-built_in': theme.secondary,
15
- 'hljs-title': theme.accent,
16
- 'hljs-variable': theme.text.primary,
17
- 'hljs-type': theme.info,
18
- 'hljs-operator': theme.text.secondary,
19
- };
7
+ /**
8
+ * 获取语法高亮颜色映射
9
+ */
10
+ function getSyntaxColors(theme) {
11
+ return {
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
+ }
20
24
  /**
21
25
  * 渲染 HAST 语法树节点
22
26
  */
23
- function renderHastNode(node, inheritedColor) {
27
+ function renderHastNode(node, inheritedColor, syntaxColors, theme) {
24
28
  if (node.type === 'text') {
25
29
  const color = inheritedColor || theme.code.text;
26
30
  return React.createElement(Text, { color: color }, node.value);
@@ -38,26 +42,26 @@ function renderHastNode(node, inheritedColor) {
38
42
  }
39
43
  const colorToPassDown = elementColor || inheritedColor;
40
44
  // 递归渲染子节点
41
- const children = node.children?.map((child, index) => (React.createElement(React.Fragment, { key: index }, renderHastNode(child, colorToPassDown))));
45
+ const children = node.children?.map((child, index) => (React.createElement(React.Fragment, { key: index }, renderHastNode(child, colorToPassDown, syntaxColors, theme))));
42
46
  return React.createElement(React.Fragment, null, children);
43
47
  }
44
48
  if (node.type === 'root') {
45
49
  if (!node.children || node.children.length === 0) {
46
50
  return null;
47
51
  }
48
- return node.children?.map((child, index) => (React.createElement(React.Fragment, { key: index }, renderHastNode(child, inheritedColor))));
52
+ return node.children?.map((child, index) => (React.createElement(React.Fragment, { key: index }, renderHastNode(child, inheritedColor, syntaxColors, theme))));
49
53
  }
50
54
  return null;
51
55
  }
52
56
  /**
53
57
  * 高亮并渲染一行代码
54
58
  */
55
- function highlightLine(line, language) {
59
+ function highlightLine(line, language, syntaxColors, theme) {
56
60
  try {
57
61
  const highlighted = !language || !lowlight.registered(language)
58
62
  ? lowlight.highlightAuto(line)
59
63
  : lowlight.highlight(language, line);
60
- const rendered = renderHastNode(highlighted, undefined);
64
+ const rendered = renderHastNode(highlighted, undefined, syntaxColors, theme);
61
65
  return rendered !== null ? rendered : line;
62
66
  }
63
67
  catch {
@@ -68,11 +72,13 @@ function highlightLine(line, language) {
68
72
  * 代码高亮组件
69
73
  */
70
74
  function ColorizeCodeInternal({ code, language = null, showLineNumbers = false }) {
75
+ const theme = getCurrentTheme();
76
+ const syntaxColors = getSyntaxColors(theme);
71
77
  const codeToHighlight = code.replace(/\n$/, '');
72
78
  const lines = codeToHighlight.split('\n');
73
79
  const padWidth = String(lines.length).length;
74
80
  const renderedLines = lines.map((line, index) => {
75
- const contentToRender = highlightLine(line, language);
81
+ const contentToRender = highlightLine(line, language, syntaxColors, theme);
76
82
  return (React.createElement(Box, { key: index, minHeight: 1 },
77
83
  showLineNumbers && (React.createElement(Text, { color: theme.text.dim }, `${String(index + 1).padStart(padWidth, ' ')} `)),
78
84
  React.createElement(Text, { color: theme.code.text }, contentToRender)));
@@ -1,11 +1,12 @@
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
  * CommandBox 组件 - 显示带边框和标题的命令框
7
7
  */
8
8
  export const CommandBox = ({ command, title = '生成命令' }) => {
9
+ const theme = getCurrentTheme();
9
10
  const lines = command.split('\n');
10
11
  const titleWidth = getDisplayWidth(title);
11
12
  const maxContentWidth = Math.max(...lines.map(l => getDisplayWidth(l)));
@@ -1,11 +1,12 @@
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
  * ConfirmationPrompt 组件 - 单键确认提示
6
6
  * 回车 = 确认,E = 编辑,Esc = 取消,Ctrl+C = 退出
7
7
  */
8
8
  export const ConfirmationPrompt = ({ prompt, onConfirm, onCancel, onEdit, }) => {
9
+ const theme = getCurrentTheme();
9
10
  useInput((input, key) => {
10
11
  if (key.return) {
11
12
  // 回车键
@@ -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
  * 格式化耗时
6
6
  */
@@ -14,6 +14,7 @@ function formatDuration(ms) {
14
14
  * Duration 组件 - 显示耗时
15
15
  */
16
16
  export const Duration = ({ ms }) => {
17
+ const theme = getCurrentTheme();
17
18
  return React.createElement(Text, { color: theme.text.secondary },
18
19
  "(",
19
20
  formatDuration(ms),
@@ -1,11 +1,12 @@
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
  * 行内 Markdown 渲染器
6
6
  * 处理 **粗体**、*斜体*、`代码`、~~删除线~~、<u>下划线</u>、链接
7
7
  */
8
8
  function RenderInlineInternal({ text, defaultColor }) {
9
+ const theme = getCurrentTheme();
9
10
  const baseColor = defaultColor || theme.text.primary;
10
11
  // 快速路径:纯文本无 markdown
11
12
  if (!/[*_~`<[https?:]/.test(text)) {
@@ -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';
@@ -12,6 +12,7 @@ import { RenderInline } from './InlineRenderer.js';
12
12
  function MarkdownDisplayInternal({ text, terminalWidth = 80 }) {
13
13
  if (!text)
14
14
  return React.createElement(React.Fragment, null);
15
+ const theme = getCurrentTheme();
15
16
  const lines = text.split(/\r?\n/);
16
17
  // 正则表达式
17
18
  const headerRegex = /^ *(#{1,4}) +(.*)/;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { type ExecutedStep } from '../multi-step.js';
2
+ import { type ExecutedStep, type RemoteContext } from '../multi-step.js';
3
3
  interface MultiStepCommandGeneratorProps {
4
4
  prompt: string;
5
5
  debug?: boolean;
@@ -18,6 +18,8 @@ interface MultiStepCommandGeneratorProps {
18
18
  }) => void;
19
19
  previousSteps?: ExecutedStep[];
20
20
  currentStepNumber?: number;
21
+ remoteContext?: RemoteContext;
22
+ isRemote?: boolean;
21
23
  }
22
24
  /**
23
25
  * MultiStepCommandGenerator 组件 - 多步骤命令生成
@@ -7,13 +7,14 @@ 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
  * MultiStepCommandGenerator 组件 - 多步骤命令生成
14
14
  * 每次只生成一个命令,支持 continue 机制
15
15
  */
16
- export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], currentStepNumber = 1, onStepComplete, }) => {
16
+ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], currentStepNumber = 1, remoteContext, isRemote = false, onStepComplete, }) => {
17
+ const theme = getCurrentTheme();
17
18
  const [state, setState] = useState({ type: 'thinking' });
18
19
  const [thinkDuration, setThinkDuration] = useState(0);
19
20
  const [debugInfo, setDebugInfo] = useState(null);
@@ -27,7 +28,7 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
27
28
  // 初始化:调用 Mastra 生成命令
28
29
  useEffect(() => {
29
30
  const thinkStart = Date.now();
30
- generateMultiStepCommand(prompt, previousSteps, { debug })
31
+ generateMultiStepCommand(prompt, previousSteps, { debug, remoteContext })
31
32
  .then((result) => {
32
33
  const thinkEnd = Date.now();
33
34
  setThinkDuration(thinkEnd - thinkStart);
@@ -48,10 +49,10 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
48
49
  }, 100);
49
50
  return;
50
51
  }
51
- // 检测 builtin(优先检测)
52
+ // 检测 builtin(优先检测,但远程执行时跳过)
52
53
  const { hasBuiltin, builtins } = detectBuiltin(result.stepData.command);
53
- if (hasBuiltin) {
54
- // 有 builtin,不管什么模式都不编辑,直接提示
54
+ if (hasBuiltin && !isRemote) {
55
+ // 有 builtin 且是本地执行,不管什么模式都不编辑,直接提示
55
56
  setState({
56
57
  type: 'showing_command',
57
58
  stepData: result.stepData,
@@ -97,7 +98,7 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
97
98
  });
98
99
  }, 100);
99
100
  });
100
- }, [prompt, previousSteps, debug]);
101
+ }, [prompt, previousSteps, debug, remoteContext]);
101
102
  // 处理确认
102
103
  const handleConfirm = () => {
103
104
  if (state.type === 'showing_command') {
@@ -175,7 +176,9 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
175
176
  React.createElement(Text, { color: theme.info },
176
177
  React.createElement(Spinner, { type: "dots" }),
177
178
  ' ',
178
- currentStepNumber === 1 ? '正在思考...' : `正在规划步骤 ${currentStepNumber}...`))),
179
+ remoteContext
180
+ ? (currentStepNumber === 1 ? `正在为 ${remoteContext.name} 思考...` : `正在规划步骤 ${currentStepNumber} (${remoteContext.name})...`)
181
+ : (currentStepNumber === 1 ? '正在思考...' : `正在规划步骤 ${currentStepNumber}...`)))),
179
182
  state.type !== 'thinking' && thinkDuration > 0 && (React.createElement(Box, null,
180
183
  React.createElement(Text, { color: theme.success }, "\u2713 \u601D\u8003\u5B8C\u6210 "),
181
184
  React.createElement(Duration, { ms: thinkDuration }))),
@@ -192,6 +195,13 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
192
195
  React.createElement(Text, { color: theme.text.secondary },
193
196
  "\u5DF2\u6267\u884C\u6B65\u9AA4\u6570: ",
194
197
  debugInfo.previousStepsCount))),
198
+ debugInfo.remoteContext && (React.createElement(Box, { marginTop: 1 },
199
+ React.createElement(Text, { color: theme.text.secondary },
200
+ "\u8FDC\u7A0B\u670D\u52A1\u5668: ",
201
+ debugInfo.remoteContext.name,
202
+ " (",
203
+ debugInfo.remoteContext.sysInfo.os,
204
+ ")"))),
195
205
  React.createElement(Box, { marginTop: 1 },
196
206
  React.createElement(Text, { color: theme.text.secondary }, "AI \u8FD4\u56DE\u7684 JSON:")),
197
207
  React.createElement(Text, { color: theme.text.dim }, JSON.stringify(debugInfo.response, null, 2)),
@@ -208,7 +218,7 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
208
218
  "\u4E0B\u4E00\u6B65: ",
209
219
  state.stepData.nextStepHint)))),
210
220
  React.createElement(CommandBox, { command: state.stepData.command }),
211
- (() => {
221
+ !isRemote && (() => {
212
222
  const { hasBuiltin, builtins } = detectBuiltin(state.stepData.command);
213
223
  if (hasBuiltin) {
214
224
  return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
@@ -220,7 +230,7 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
220
230
  }
221
231
  return null;
222
232
  })(),
223
- !detectBuiltin(state.stepData.command).hasBuiltin && (React.createElement(ConfirmationPrompt, { prompt: "\u6267\u884C\uFF1F", onConfirm: handleConfirm, onCancel: handleCancel, onEdit: handleEdit })))),
233
+ (isRemote || !detectBuiltin(state.stepData.command).hasBuiltin) && (React.createElement(ConfirmationPrompt, { prompt: "\u6267\u884C\uFF1F", onConfirm: handleConfirm, onCancel: handleCancel, onEdit: handleEdit })))),
224
234
  state.type === 'editing' && (React.createElement(React.Fragment, null,
225
235
  state.stepData.continue === true && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
226
236
  React.createElement(Text, { color: theme.text.secondary },
@@ -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
  * 计算纯文本长度(去除 markdown 标记)
@@ -40,6 +40,7 @@ function calculateColumnWidths(headers, rows, terminalWidth) {
40
40
  * 表格渲染组件
41
41
  */
42
42
  function TableRendererInternal({ headers, rows, terminalWidth }) {
43
+ const theme = getCurrentTheme();
43
44
  const columnWidths = calculateColumnWidths(headers, rows, terminalWidth);
44
45
  const baseColor = theme.text.primary;
45
46
  return (React.createElement(Box, { flexDirection: "column", marginY: 1 },