@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,82 @@
1
+ import React from 'react';
2
+ import { Text, Box } from 'ink';
3
+ import { common, createLowlight } from 'lowlight';
4
+ import { theme } from '../ui/theme.js';
5
+ // 创建 lowlight 实例
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
+ };
20
+ /**
21
+ * 渲染 HAST 语法树节点
22
+ */
23
+ function renderHastNode(node, inheritedColor) {
24
+ if (node.type === 'text') {
25
+ const color = inheritedColor || theme.code.text;
26
+ return React.createElement(Text, { color: color }, node.value);
27
+ }
28
+ if (node.type === 'element') {
29
+ const nodeClasses = node.properties?.['className'] || [];
30
+ let elementColor = undefined;
31
+ // 查找颜色
32
+ for (let i = nodeClasses.length - 1; i >= 0; i--) {
33
+ const className = nodeClasses[i];
34
+ if (syntaxColors[className]) {
35
+ elementColor = syntaxColors[className];
36
+ break;
37
+ }
38
+ }
39
+ const colorToPassDown = elementColor || inheritedColor;
40
+ // 递归渲染子节点
41
+ const children = node.children?.map((child, index) => (React.createElement(React.Fragment, { key: index }, renderHastNode(child, colorToPassDown))));
42
+ return React.createElement(React.Fragment, null, children);
43
+ }
44
+ if (node.type === 'root') {
45
+ if (!node.children || node.children.length === 0) {
46
+ return null;
47
+ }
48
+ return node.children?.map((child, index) => (React.createElement(React.Fragment, { key: index }, renderHastNode(child, inheritedColor))));
49
+ }
50
+ return null;
51
+ }
52
+ /**
53
+ * 高亮并渲染一行代码
54
+ */
55
+ function highlightLine(line, language) {
56
+ try {
57
+ const highlighted = !language || !lowlight.registered(language)
58
+ ? lowlight.highlightAuto(line)
59
+ : lowlight.highlight(language, line);
60
+ const rendered = renderHastNode(highlighted, undefined);
61
+ return rendered !== null ? rendered : line;
62
+ }
63
+ catch {
64
+ return line;
65
+ }
66
+ }
67
+ /**
68
+ * 代码高亮组件
69
+ */
70
+ function ColorizeCodeInternal({ code, language = null, showLineNumbers = false }) {
71
+ const codeToHighlight = code.replace(/\n$/, '');
72
+ const lines = codeToHighlight.split('\n');
73
+ const padWidth = String(lines.length).length;
74
+ const renderedLines = lines.map((line, index) => {
75
+ const contentToRender = highlightLine(line, language);
76
+ return (React.createElement(Box, { key: index, minHeight: 1 },
77
+ showLineNumbers && (React.createElement(Text, { color: theme.text.dim }, `${String(index + 1).padStart(padWidth, ' ')} `)),
78
+ React.createElement(Text, { color: theme.code.text }, contentToRender)));
79
+ });
80
+ return (React.createElement(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, paddingY: 1, borderStyle: "round", borderColor: theme.border }, renderedLines));
81
+ }
82
+ export const ColorizeCode = React.memo(ColorizeCodeInternal);
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ interface CommandBoxProps {
3
+ command: string;
4
+ title?: string;
5
+ }
6
+ /**
7
+ * CommandBox 组件 - 显示带边框和标题的命令框
8
+ */
9
+ export declare const CommandBox: React.FC<CommandBoxProps>;
10
+ export {};
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { theme } from '../ui/theme.js';
4
+ /**
5
+ * 计算字符串的显示宽度(中文占2个宽度)
6
+ */
7
+ function getDisplayWidth(str) {
8
+ let width = 0;
9
+ for (const char of str) {
10
+ // 中文、日文、韩文等宽字符占 2 个宽度
11
+ if (char.match(/[\u4e00-\u9fff\u3400-\u4dbf\uff00-\uffef\u3000-\u303f]/)) {
12
+ width += 2;
13
+ }
14
+ else {
15
+ width += 1;
16
+ }
17
+ }
18
+ return width;
19
+ }
20
+ /**
21
+ * CommandBox 组件 - 显示带边框和标题的命令框
22
+ */
23
+ export const CommandBox = ({ command, title = '生成命令' }) => {
24
+ const lines = command.split('\n');
25
+ const titleWidth = getDisplayWidth(title);
26
+ const maxContentWidth = Math.max(...lines.map(l => getDisplayWidth(l)));
27
+ const boxWidth = Math.max(maxContentWidth + 4, titleWidth + 6, 20);
28
+ // 顶部边框:┌─ 生成命令 ─────┐
29
+ const topPadding = boxWidth - titleWidth - 5;
30
+ const topBorder = '┌─ ' + title + ' ' + '─'.repeat(topPadding) + '┐';
31
+ // 底部边框
32
+ const bottomBorder = '└' + '─'.repeat(boxWidth - 2) + '┘';
33
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
34
+ React.createElement(Text, { color: theme.warning }, topBorder),
35
+ lines.map((line, index) => {
36
+ const lineWidth = getDisplayWidth(line);
37
+ const padding = ' '.repeat(boxWidth - lineWidth - 4);
38
+ return (React.createElement(Text, { key: index },
39
+ React.createElement(Text, { color: theme.warning }, "\u2502 "),
40
+ React.createElement(Text, { color: theme.primary }, line),
41
+ React.createElement(Text, null, padding),
42
+ React.createElement(Text, { color: theme.warning }, " \u2502")));
43
+ }),
44
+ React.createElement(Text, { color: theme.warning }, bottomBorder)));
45
+ };
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ interface CommandGeneratorProps {
3
+ prompt: string;
4
+ debug?: boolean;
5
+ onComplete: (result: {
6
+ command?: string;
7
+ confirmed?: boolean;
8
+ cancelled?: boolean;
9
+ hasBuiltin?: boolean;
10
+ builtins?: string[];
11
+ debugInfo?: any;
12
+ error?: string;
13
+ }) => void;
14
+ }
15
+ /**
16
+ * CommandGenerator 组件 - 命令生成和确认(仅用于交互)
17
+ * 不执行命令,执行交给调用方用原生方式处理
18
+ */
19
+ export declare const CommandGenerator: React.FC<CommandGeneratorProps>;
20
+ export {};
@@ -0,0 +1,116 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import { generateCommand } from '../ai.js';
5
+ import { detectBuiltin, formatBuiltins } from '../builtin-detector.js';
6
+ import { CommandBox } from './CommandBox.js';
7
+ import { ConfirmationPrompt } from './ConfirmationPrompt.js';
8
+ import { Duration } from './Duration.js';
9
+ import { theme } from '../ui/theme.js';
10
+ /**
11
+ * CommandGenerator 组件 - 命令生成和确认(仅用于交互)
12
+ * 不执行命令,执行交给调用方用原生方式处理
13
+ */
14
+ export const CommandGenerator = ({ prompt, debug, onComplete }) => {
15
+ const [state, setState] = useState({ type: 'thinking' });
16
+ const [thinkDuration, setThinkDuration] = useState(0);
17
+ const [debugInfo, setDebugInfo] = useState(null);
18
+ // 初始化:调用 AI 生成命令
19
+ useEffect(() => {
20
+ const thinkStart = Date.now();
21
+ generateCommand(prompt, { debug: debug || false })
22
+ .then((result) => {
23
+ const command = debug && typeof result === 'object' ? result.command : result;
24
+ const thinkEnd = Date.now();
25
+ setThinkDuration(thinkEnd - thinkStart);
26
+ if (debug && typeof result === 'object' && 'debug' in result) {
27
+ setDebugInfo(result.debug);
28
+ }
29
+ // 检测 builtin
30
+ const { hasBuiltin, builtins } = detectBuiltin(command);
31
+ setState({
32
+ type: 'showing_command',
33
+ command,
34
+ hasBuiltin,
35
+ builtins,
36
+ });
37
+ // 如果是 builtin,直接完成(不执行)
38
+ if (hasBuiltin) {
39
+ setTimeout(() => {
40
+ onComplete({
41
+ command,
42
+ confirmed: false,
43
+ hasBuiltin: true,
44
+ builtins,
45
+ debugInfo: debugInfo || undefined,
46
+ });
47
+ }, 100);
48
+ }
49
+ })
50
+ .catch((error) => {
51
+ setState({ type: 'error', error: error.message });
52
+ setTimeout(() => {
53
+ onComplete({ error: error.message });
54
+ }, 100);
55
+ });
56
+ }, [prompt, debug]);
57
+ // 处理确认
58
+ const handleConfirm = () => {
59
+ if (state.type === 'showing_command') {
60
+ // 返回命令和确认状态,让调用方执行
61
+ onComplete({
62
+ command: state.command,
63
+ confirmed: true,
64
+ debugInfo: debugInfo || undefined,
65
+ });
66
+ }
67
+ };
68
+ // 处理取消
69
+ const handleCancel = () => {
70
+ if (state.type === 'showing_command') {
71
+ setState({ type: 'cancelled', command: state.command });
72
+ setTimeout(() => {
73
+ onComplete({
74
+ command: state.command,
75
+ cancelled: true,
76
+ });
77
+ }, 100);
78
+ }
79
+ };
80
+ return (React.createElement(Box, { flexDirection: "column" },
81
+ state.type === 'thinking' && (React.createElement(Box, null,
82
+ React.createElement(Text, { color: theme.info },
83
+ React.createElement(Spinner, { type: "dots" }),
84
+ " \u6B63\u5728\u601D\u8003..."))),
85
+ state.type !== 'thinking' && thinkDuration > 0 && (React.createElement(Box, null,
86
+ React.createElement(Text, { color: theme.success }, "\u2713 \u601D\u8003\u5B8C\u6210 "),
87
+ React.createElement(Duration, { ms: thinkDuration }))),
88
+ debugInfo && (React.createElement(Box, { flexDirection: "column", marginY: 1 },
89
+ React.createElement(Text, { color: theme.accent }, "\u2501\u2501\u2501 \u8C03\u8BD5\u4FE1\u606F \u2501\u2501\u2501"),
90
+ React.createElement(Text, { color: theme.text.secondary },
91
+ "\u7CFB\u7EDF\u4FE1\u606F: ",
92
+ debugInfo.sysinfo),
93
+ React.createElement(Text, { color: theme.text.secondary },
94
+ "\u6A21\u578B: ",
95
+ debugInfo.model),
96
+ React.createElement(Text, { color: theme.text.secondary }, "System Prompt:"),
97
+ React.createElement(Text, { dimColor: true }, debugInfo.systemPrompt),
98
+ React.createElement(Text, { color: theme.text.secondary },
99
+ "User Prompt: ",
100
+ debugInfo.userPrompt),
101
+ React.createElement(Text, { color: theme.accent }, "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"))),
102
+ (state.type === 'showing_command' || state.type === 'cancelled') && (React.createElement(CommandBox, { command: state.command })),
103
+ state.type === 'showing_command' && state.hasBuiltin && (React.createElement(Box, { flexDirection: "column", marginY: 1 },
104
+ React.createElement(Text, { color: theme.error },
105
+ "\u26A0\uFE0F \u6B64\u547D\u4EE4\u5305\u542B shell \u5185\u7F6E\u547D\u4EE4\uFF08",
106
+ formatBuiltins(state.builtins),
107
+ "\uFF09\uFF0C\u65E0\u6CD5\u5728\u5B50\u8FDB\u7A0B\u4E2D\u751F\u6548"),
108
+ React.createElement(Text, { color: theme.warning }, "\uD83D\uDCA1 \u8BF7\u624B\u52A8\u590D\u5236\u5230\u7EC8\u7AEF\u6267\u884C"))),
109
+ state.type === 'showing_command' && !state.hasBuiltin && (React.createElement(ConfirmationPrompt, { prompt: "\u6267\u884C\uFF1F", onConfirm: handleConfirm, onCancel: handleCancel })),
110
+ state.type === 'cancelled' && (React.createElement(Box, { marginTop: 1 },
111
+ React.createElement(Text, { color: theme.text.secondary }, "\u5DF2\u53D6\u6D88\u6267\u884C"))),
112
+ state.type === 'error' && (React.createElement(Box, { marginTop: 1 },
113
+ React.createElement(Text, { color: theme.error },
114
+ "\u274C \u9519\u8BEF: ",
115
+ state.error)))));
116
+ };
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface ConfigDisplayProps {
3
+ onComplete?: () => void;
4
+ }
5
+ /**
6
+ * ConfigDisplay 组件 - 显示当前配置
7
+ */
8
+ export declare const ConfigDisplay: React.FC<ConfigDisplayProps>;
9
+ export {};
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { getConfig, maskApiKey } from '../config.js';
4
+ import { theme } from '../ui/theme.js';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ const CONFIG_FILE = path.join(os.homedir(), '.please', 'config.json');
8
+ /**
9
+ * ConfigDisplay 组件 - 显示当前配置
10
+ */
11
+ export const ConfigDisplay = ({ onComplete }) => {
12
+ const config = getConfig();
13
+ React.useEffect(() => {
14
+ if (onComplete) {
15
+ setTimeout(onComplete, 100);
16
+ }
17
+ }, [onComplete]);
18
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
19
+ React.createElement(Text, { bold: true }, "\u5F53\u524D\u914D\u7F6E:"),
20
+ React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
21
+ React.createElement(Box, null,
22
+ React.createElement(Text, { color: theme.primary }, " apiKey: "),
23
+ React.createElement(Text, null, maskApiKey(config.apiKey))),
24
+ React.createElement(Box, null,
25
+ React.createElement(Text, { color: theme.primary }, " baseUrl: "),
26
+ React.createElement(Text, null, config.baseUrl)),
27
+ React.createElement(Box, null,
28
+ React.createElement(Text, { color: theme.primary }, " model: "),
29
+ React.createElement(Text, null, config.model)),
30
+ React.createElement(Box, null,
31
+ React.createElement(Text, { color: theme.primary }, " shellHook: "),
32
+ config.shellHook ? (React.createElement(Text, { color: theme.success }, "\u5DF2\u542F\u7528")) : (React.createElement(Text, { color: theme.text.secondary }, "\u672A\u542F\u7528"))),
33
+ React.createElement(Box, null,
34
+ React.createElement(Text, { color: theme.primary }, " chatHistoryLimit: "),
35
+ React.createElement(Text, null,
36
+ config.chatHistoryLimit,
37
+ " \u8F6E")),
38
+ React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
39
+ React.createElement(Text, { color: theme.text.secondary },
40
+ "\u914D\u7F6E\u6587\u4EF6: ",
41
+ CONFIG_FILE)));
42
+ };
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface ConfigWizardProps {
3
+ onComplete: () => void;
4
+ }
5
+ /**
6
+ * ConfigWizard 组件 - 交互式配置向导
7
+ */
8
+ export declare const ConfigWizard: React.FC<ConfigWizardProps>;
9
+ export {};
@@ -0,0 +1,72 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import { getConfig, saveConfig, maskApiKey } from '../config.js';
5
+ import { theme } from '../ui/theme.js';
6
+ import path from 'path';
7
+ import os from 'os';
8
+ const CONFIG_FILE = path.join(os.homedir(), '.please', 'config.json');
9
+ /**
10
+ * ConfigWizard 组件 - 交互式配置向导
11
+ */
12
+ export const ConfigWizard = ({ onComplete }) => {
13
+ const config = getConfig();
14
+ const [step, setStep] = useState('apiKey');
15
+ const [apiKey, setApiKey] = useState(config.apiKey);
16
+ const [baseUrl, setBaseUrl] = useState(config.baseUrl);
17
+ const [model, setModel] = useState(config.model);
18
+ const handleApiKeySubmit = (value) => {
19
+ if (value.trim()) {
20
+ setApiKey(value.trim());
21
+ }
22
+ setStep('baseUrl');
23
+ };
24
+ const handleBaseUrlSubmit = (value) => {
25
+ if (value.trim()) {
26
+ setBaseUrl(value.trim());
27
+ }
28
+ setStep('model');
29
+ };
30
+ const handleModelSubmit = (value) => {
31
+ if (value.trim()) {
32
+ setModel(value.trim());
33
+ }
34
+ // 保存配置
35
+ saveConfig({
36
+ ...config,
37
+ apiKey: apiKey || config.apiKey,
38
+ baseUrl: baseUrl || config.baseUrl,
39
+ model: model.trim() || config.model,
40
+ });
41
+ setStep('done');
42
+ setTimeout(onComplete, 100);
43
+ };
44
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
45
+ React.createElement(Text, { bold: true, color: theme.accent }, "\uD83D\uDD27 Pretty Please \u914D\u7F6E\u5411\u5BFC"),
46
+ React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
47
+ step === 'apiKey' && (React.createElement(Box, { marginTop: 1 },
48
+ React.createElement(Text, { color: theme.primary },
49
+ "\u8BF7\u8F93\u5165 API Key",
50
+ config.apiKey ? ` (当前: ${maskApiKey(config.apiKey)})` : '',
51
+ ":",
52
+ ' '),
53
+ React.createElement(TextInput, { value: "", onChange: () => { }, onSubmit: handleApiKeySubmit }))),
54
+ step === 'baseUrl' && (React.createElement(Box, { marginTop: 1 },
55
+ React.createElement(Text, { color: theme.primary },
56
+ "\u8BF7\u8F93\u5165 API Base URL (\u56DE\u8F66\u4F7F\u7528 ",
57
+ baseUrl,
58
+ "):",
59
+ ' '),
60
+ React.createElement(TextInput, { value: "", onChange: () => { }, onSubmit: handleBaseUrlSubmit }))),
61
+ step === 'model' && (React.createElement(Box, { marginTop: 1 },
62
+ React.createElement(Text, { color: theme.primary },
63
+ "\u8BF7\u8F93\u5165\u6A21\u578B\u540D\u79F0 (\u56DE\u8F66\u4F7F\u7528 ",
64
+ model,
65
+ "):",
66
+ ' '),
67
+ React.createElement(TextInput, { value: "", onChange: () => { }, onSubmit: handleModelSubmit }))),
68
+ step === 'done' && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
69
+ React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
70
+ React.createElement(Text, { color: theme.success }, "\u2705 \u914D\u7F6E\u5DF2\u4FDD\u5B58\u5230 "),
71
+ React.createElement(Text, { color: theme.text.secondary }, CONFIG_FILE)))));
72
+ };
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ interface ConfirmationPromptProps {
3
+ prompt: string;
4
+ onConfirm: () => void;
5
+ onCancel: () => void;
6
+ }
7
+ /**
8
+ * ConfirmationPrompt 组件 - 单键确认提示
9
+ * 回车 = 确认,Esc = 取消,Ctrl+C = 退出
10
+ */
11
+ export declare const ConfirmationPrompt: React.FC<ConfirmationPromptProps>;
12
+ export {};
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { Text, useInput } from 'ink';
3
+ import { theme } from '../ui/theme.js';
4
+ /**
5
+ * ConfirmationPrompt 组件 - 单键确认提示
6
+ * 回车 = 确认,Esc = 取消,Ctrl+C = 退出
7
+ */
8
+ export const ConfirmationPrompt = ({ prompt, onConfirm, onCancel, }) => {
9
+ useInput((input, key) => {
10
+ if (key.return) {
11
+ // 回车键
12
+ onConfirm();
13
+ }
14
+ else if (key.escape) {
15
+ // Esc 键
16
+ onCancel();
17
+ }
18
+ else if (key.ctrl && input === 'c') {
19
+ // Ctrl+C
20
+ process.exit(0);
21
+ }
22
+ });
23
+ return (React.createElement(Text, null,
24
+ React.createElement(Text, { bold: true, color: theme.warning }, prompt),
25
+ React.createElement(Text, { color: theme.text.secondary }, " [\u56DE\u8F66\u6267\u884C / Esc \u53D6\u6D88] ")));
26
+ };
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface DurationProps {
3
+ ms: number;
4
+ }
5
+ /**
6
+ * Duration 组件 - 显示耗时
7
+ */
8
+ export declare const Duration: React.FC<DurationProps>;
9
+ export {};
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { Text } from 'ink';
3
+ import { theme } from '../ui/theme.js';
4
+ /**
5
+ * 格式化耗时
6
+ */
7
+ function formatDuration(ms) {
8
+ if (ms < 1000) {
9
+ return `${ms}ms`;
10
+ }
11
+ return `${(ms / 1000).toFixed(2)}s`;
12
+ }
13
+ /**
14
+ * Duration 组件 - 显示耗时
15
+ */
16
+ export const Duration = ({ ms }) => {
17
+ return React.createElement(Text, { color: theme.text.secondary },
18
+ "(",
19
+ formatDuration(ms),
20
+ ")");
21
+ };
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface HistoryDisplayProps {
3
+ onComplete?: () => void;
4
+ }
5
+ /**
6
+ * HistoryDisplay 组件 - 显示历史记录
7
+ */
8
+ export declare const HistoryDisplay: React.FC<HistoryDisplayProps>;
9
+ export {};
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { getHistory, getHistoryFilePath } from '../history.js';
4
+ import { theme } from '../ui/theme.js';
5
+ /**
6
+ * HistoryDisplay 组件 - 显示历史记录
7
+ */
8
+ export const HistoryDisplay = ({ onComplete }) => {
9
+ const history = getHistory();
10
+ React.useEffect(() => {
11
+ if (onComplete) {
12
+ setTimeout(onComplete, 100);
13
+ }
14
+ }, [onComplete]);
15
+ if (history.length === 0) {
16
+ return (React.createElement(Box, { marginY: 1 },
17
+ React.createElement(Text, { color: theme.text.secondary }, "\u6682\u65E0\u5386\u53F2\u8BB0\u5F55")));
18
+ }
19
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
20
+ React.createElement(Text, { bold: true }, "\uD83D\uDCDC \u547D\u4EE4\u5386\u53F2:"),
21
+ React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(50)),
22
+ history.map((item, index) => {
23
+ const status = item.executed
24
+ ? item.exitCode === 0
25
+ ? '✓'
26
+ : `✗ 退出码:${item.exitCode}`
27
+ : '(未执行)';
28
+ const statusColor = item.executed
29
+ ? item.exitCode === 0
30
+ ? theme.success
31
+ : theme.error
32
+ : theme.text.secondary;
33
+ return (React.createElement(Box, { key: index, flexDirection: "column", marginY: 1 },
34
+ React.createElement(Box, null,
35
+ React.createElement(Text, { color: theme.text.secondary },
36
+ index + 1,
37
+ ". "),
38
+ React.createElement(Text, { color: theme.primary }, item.userPrompt)),
39
+ React.createElement(Box, { marginLeft: 3 },
40
+ React.createElement(Text, { dimColor: true }, "\u2192 "),
41
+ React.createElement(Text, null,
42
+ item.command,
43
+ " "),
44
+ React.createElement(Text, { color: statusColor }, status)),
45
+ React.createElement(Box, { marginLeft: 3 },
46
+ React.createElement(Text, { color: theme.text.secondary }, item.timestamp))));
47
+ }),
48
+ React.createElement(Text, { color: theme.text.secondary },
49
+ "\u5386\u53F2\u6587\u4EF6: ",
50
+ getHistoryFilePath())));
51
+ };
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ interface HookManagerProps {
3
+ action: 'status' | 'install' | 'uninstall';
4
+ onComplete: () => void;
5
+ }
6
+ /**
7
+ * HookManager 组件 - Hook 管理界面
8
+ */
9
+ export declare const HookManager: React.FC<HookManagerProps>;
10
+ export {};
@@ -0,0 +1,88 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { getHookStatus, installShellHook, uninstallShellHook, detectShell, getShellConfigPath, } from '../shell-hook.js';
4
+ import { theme } from '../ui/theme.js';
5
+ /**
6
+ * HookManager 组件 - Hook 管理界面
7
+ */
8
+ export const HookManager = ({ action, onComplete }) => {
9
+ const [status, setStatus] = useState(getHookStatus());
10
+ const [message, setMessage] = useState(null);
11
+ const [isProcessing, setIsProcessing] = useState(false);
12
+ useEffect(() => {
13
+ const execute = async () => {
14
+ if (action === 'install') {
15
+ setIsProcessing(true);
16
+ const shellType = detectShell();
17
+ const configPath = getShellConfigPath(shellType);
18
+ if (shellType === 'unknown') {
19
+ setMessage('❌ 不支持的 shell 类型');
20
+ setIsProcessing(false);
21
+ setTimeout(onComplete, 2000);
22
+ return;
23
+ }
24
+ const result = await installShellHook();
25
+ setStatus(getHookStatus());
26
+ setIsProcessing(false);
27
+ if (result) {
28
+ setMessage(`✅ Shell hook 已安装\n⚠️ 请重启终端或执行: source ${configPath}`);
29
+ }
30
+ setTimeout(onComplete, 3000);
31
+ }
32
+ else if (action === 'uninstall') {
33
+ setIsProcessing(true);
34
+ uninstallShellHook();
35
+ setStatus(getHookStatus());
36
+ setMessage('✅ Shell hook 已卸载\n⚠️ 请重启终端使其生效');
37
+ setIsProcessing(false);
38
+ setTimeout(onComplete, 3000);
39
+ }
40
+ else {
41
+ // status
42
+ setTimeout(onComplete, 100);
43
+ }
44
+ };
45
+ execute();
46
+ }, [action, onComplete]);
47
+ if (action === 'install' || action === 'uninstall') {
48
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
49
+ React.createElement(Text, { bold: true, color: theme.accent },
50
+ "\uD83D\uDD27 Shell Hook ",
51
+ action === 'install' ? '安装' : '卸载',
52
+ "\u5411\u5BFC"),
53
+ React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
54
+ isProcessing && React.createElement(Text, { color: theme.info }, "\u5904\u7406\u4E2D..."),
55
+ message && (React.createElement(Box, { flexDirection: "column", marginTop: 1 }, message.split('\n').map((line, i) => (React.createElement(Text, { key: i, color: line.startsWith('✅')
56
+ ? theme.success
57
+ : line.startsWith('⚠️')
58
+ ? theme.warning
59
+ : line.startsWith('❌')
60
+ ? theme.error
61
+ : theme.text.primary }, line)))))));
62
+ }
63
+ // Status display
64
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
65
+ React.createElement(Text, { bold: true }, "\uD83D\uDCCA Shell Hook \u72B6\u6001"),
66
+ React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
67
+ React.createElement(Box, { marginTop: 1 },
68
+ React.createElement(Text, { color: theme.primary }, " Shell \u7C7B\u578B: "),
69
+ React.createElement(Text, null, status.shellType)),
70
+ React.createElement(Box, null,
71
+ React.createElement(Text, { color: theme.primary }, " \u914D\u7F6E\u6587\u4EF6: "),
72
+ React.createElement(Text, null, status.configPath || '未知')),
73
+ React.createElement(Box, null,
74
+ React.createElement(Text, { color: theme.primary }, " \u5DF2\u5B89\u88C5: "),
75
+ status.installed ? (React.createElement(Text, { color: theme.success }, "\u662F")) : (React.createElement(Text, { color: theme.text.secondary }, "\u5426"))),
76
+ React.createElement(Box, null,
77
+ React.createElement(Text, { color: theme.primary }, " \u5DF2\u542F\u7528: "),
78
+ status.enabled ? (React.createElement(Text, { color: theme.success }, "\u662F")) : (React.createElement(Text, { color: theme.text.secondary }, "\u5426"))),
79
+ React.createElement(Box, null,
80
+ React.createElement(Text, { color: theme.primary }, " \u5386\u53F2\u6587\u4EF6: "),
81
+ React.createElement(Text, null, status.historyFile)),
82
+ React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
83
+ !status.installed && (React.createElement(Box, { marginTop: 1 },
84
+ React.createElement(Text, { color: theme.text.secondary },
85
+ "\u63D0\u793A: \u8FD0\u884C ",
86
+ React.createElement(Text, { color: theme.primary }, "pls hook install"),
87
+ " \u5B89\u88C5 shell hook")))));
88
+ };
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ interface RenderInlineProps {
3
+ text: string;
4
+ defaultColor?: string;
5
+ }
6
+ /**
7
+ * 行内 Markdown 渲染器
8
+ * 处理 **粗体**、*斜体*、`代码`、~~删除线~~、<u>下划线</u>、链接
9
+ */
10
+ declare function RenderInlineInternal({ text, defaultColor }: RenderInlineProps): React.JSX.Element;
11
+ export declare const RenderInline: React.MemoExoticComponent<typeof RenderInlineInternal>;
12
+ export {};