@hunterzhu/pulse-cli 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.d.ts +10 -1
- package/dist/bin.js +86 -177
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +36 -0
- package/dist/commands/interactive.d.ts +2 -0
- package/dist/commands/interactive.js +27 -0
- package/dist/commands/resume.d.ts +2 -0
- package/dist/commands/resume.js +75 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +73 -0
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +66 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +30 -0
- package/dist/commands/signals.d.ts +2 -0
- package/dist/commands/signals.js +20 -0
- package/dist/components/App.d.ts +10 -0
- package/dist/components/App.js +364 -0
- package/dist/components/ApprovalPrompt.d.ts +8 -0
- package/dist/components/ApprovalPrompt.js +21 -0
- package/dist/components/AssistantMessage.d.ts +10 -0
- package/dist/components/AssistantMessage.js +10 -0
- package/dist/components/Header.d.ts +8 -0
- package/dist/components/Header.js +8 -0
- package/dist/components/HelpView.d.ts +1 -0
- package/dist/components/HelpView.js +44 -0
- package/dist/components/InputArea.d.ts +7 -0
- package/dist/components/InputArea.js +55 -0
- package/dist/components/MessageList.d.ts +8 -0
- package/dist/components/MessageList.js +12 -0
- package/dist/components/SessionList.d.ts +15 -0
- package/dist/components/SessionList.js +33 -0
- package/dist/components/Spinner.d.ts +4 -0
- package/dist/components/Spinner.js +7 -0
- package/dist/components/ThinkingBlock.d.ts +5 -0
- package/dist/components/ThinkingBlock.js +8 -0
- package/dist/components/TokenStats.d.ts +7 -0
- package/dist/components/TokenStats.js +15 -0
- package/dist/components/ToolCallCard.d.ts +8 -0
- package/dist/components/ToolCallCard.js +31 -0
- package/dist/components/UserMessage.d.ts +5 -0
- package/dist/components/UserMessage.js +7 -0
- package/dist/components/Welcome.d.ts +6 -0
- package/dist/components/Welcome.js +6 -0
- package/dist/hooks/useConversation.d.ts +15 -0
- package/dist/hooks/useConversation.js +115 -0
- package/dist/hooks/useHost.d.ts +7 -0
- package/dist/hooks/useHost.js +44 -0
- package/dist/hooks/useMultilineInput.d.ts +11 -0
- package/dist/hooks/useMultilineInput.js +58 -0
- package/dist/hooks/useRun.d.ts +17 -0
- package/dist/hooks/useRun.js +213 -0
- package/dist/hooks/useSlashCommands.d.ts +26 -0
- package/dist/hooks/useSlashCommands.js +45 -0
- package/dist/hooks/useTokenStats.d.ts +7 -0
- package/dist/hooks/useTokenStats.js +38 -0
- package/dist/theme.d.ts +27 -0
- package/dist/theme.js +27 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +1 -0
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +20 -0
- package/dist/utils/approval.d.ts +10 -0
- package/dist/utils/approval.js +50 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.js +51 -0
- package/dist/utils/highlight.d.ts +1 -0
- package/dist/utils/highlight.js +80 -0
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +138 -0
- package/package.json +13 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { defaultPulseConfig, defaultPulseConfigPath } from '../config.js';
|
|
5
|
+
export async function runSetup(force, explicitPath) {
|
|
6
|
+
try {
|
|
7
|
+
const configPath = explicitPath ? path.resolve(explicitPath) : defaultPulseConfigPath();
|
|
8
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
9
|
+
let exists = false;
|
|
10
|
+
try {
|
|
11
|
+
await fs.access(configPath);
|
|
12
|
+
exists = true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
exists = false;
|
|
16
|
+
}
|
|
17
|
+
if (exists && !force) {
|
|
18
|
+
console.log(chalk.yellow(`Config file already exists at ${configPath}. Use --force to overwrite.`));
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
await fs.writeFile(configPath, `${JSON.stringify(defaultPulseConfig, null, 2)}\n`, { mode: 0o600 });
|
|
22
|
+
await fs.chmod(configPath, 0o600);
|
|
23
|
+
console.log(chalk.green(`✓ Created config file at ${configPath}`));
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error(chalk.red('Failed to setup config:'), error);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function bindRunSignals(host, getRun) {
|
|
2
|
+
let interrupted = false;
|
|
3
|
+
const onSignal = () => {
|
|
4
|
+
const run = getRun();
|
|
5
|
+
if (interrupted || !run) {
|
|
6
|
+
void host.close().finally(() => {
|
|
7
|
+
process.exit(130);
|
|
8
|
+
});
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
interrupted = true;
|
|
12
|
+
void run.cancel('USER_INTERRUPT');
|
|
13
|
+
};
|
|
14
|
+
process.on('SIGINT', onSignal);
|
|
15
|
+
process.on('SIGTERM', onSignal);
|
|
16
|
+
return () => {
|
|
17
|
+
process.off('SIGINT', onSignal);
|
|
18
|
+
process.off('SIGTERM', onSignal);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { LocalHostOptions } from '@hunterzhu/pulse-server';
|
|
3
|
+
export interface AppProps {
|
|
4
|
+
hostOptions: LocalHostOptions;
|
|
5
|
+
conversationId?: string | undefined;
|
|
6
|
+
initialTask?: string | undefined;
|
|
7
|
+
version: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function App({ hostOptions, conversationId: initialConversationId, initialTask, version, }: AppProps): React.JSX.Element;
|
|
10
|
+
export default App;
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
+
import { Header } from './Header.js';
|
|
5
|
+
import { Welcome } from './Welcome.js';
|
|
6
|
+
import { MessageList } from './MessageList.js';
|
|
7
|
+
import { Spinner } from './Spinner.js';
|
|
8
|
+
import { ApprovalPrompt } from './ApprovalPrompt.js';
|
|
9
|
+
import { InputArea } from './InputArea.js';
|
|
10
|
+
import { SessionList } from './SessionList.js';
|
|
11
|
+
import { HelpView } from './HelpView.js';
|
|
12
|
+
import { useHost } from '../hooks/useHost.js';
|
|
13
|
+
import { useConversation } from '../hooks/useConversation.js';
|
|
14
|
+
import { useRun } from '../hooks/useRun.js';
|
|
15
|
+
import { useTokenStats } from '../hooks/useTokenStats.js';
|
|
16
|
+
import { useSlashCommands } from '../hooks/useSlashCommands.js';
|
|
17
|
+
export function App({ hostOptions, conversationId: initialConversationId, initialTask, version, }) {
|
|
18
|
+
const { exit } = useApp();
|
|
19
|
+
const [mode, setMode] = useState('chat');
|
|
20
|
+
const [showThinking, setShowThinking] = useState(false);
|
|
21
|
+
const [verbosity, setVerbosity] = useState('normal');
|
|
22
|
+
const [sessionsList, setSessionsList] = useState([]);
|
|
23
|
+
const [sessionError, setSessionError] = useState(null);
|
|
24
|
+
const startedTask = useRef(false);
|
|
25
|
+
const resumedConversation = useRef(null);
|
|
26
|
+
const { host, error: hostError, ready: hostReady } = useHost(hostOptions);
|
|
27
|
+
const { conversation, messages, addUserMessage, addAssistantMessage, clearMessages, switchConversation, newConversation, error: conversationError, } = useConversation({ host, conversationId: initialConversationId });
|
|
28
|
+
const { isRunning, currentStep, error: runError, approvalRequest, sendMessage, resumeActive, approveAction, cancelRun, } = useRun({
|
|
29
|
+
host,
|
|
30
|
+
conversationId: conversation?.id ?? null,
|
|
31
|
+
addAssistantMessage,
|
|
32
|
+
});
|
|
33
|
+
const { cumulative } = useTokenStats();
|
|
34
|
+
const loadSessions = useCallback(async () => {
|
|
35
|
+
if (!host)
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
const list = await host.listConversations();
|
|
39
|
+
setSessionsList(list);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// ignore
|
|
43
|
+
}
|
|
44
|
+
}, [host]);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!hostReady || !conversation || isRunning)
|
|
47
|
+
return;
|
|
48
|
+
if (conversation.summary.activeRunId && resumedConversation.current !== conversation.id) {
|
|
49
|
+
resumedConversation.current = conversation.id;
|
|
50
|
+
if (initialTask && !startedTask.current) {
|
|
51
|
+
startedTask.current = true;
|
|
52
|
+
addAssistantMessage({
|
|
53
|
+
id: `resume-hold-${Date.now()}`,
|
|
54
|
+
role: 'system',
|
|
55
|
+
text: '此会话有未完成的运行,已改为恢复该运行。这次附带的新任务没有发送。',
|
|
56
|
+
createdAt: new Date().toISOString(),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
void resumeActive();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (initialTask && !startedTask.current && messages.length === 0 && !conversation.summary.activeRunId) {
|
|
63
|
+
startedTask.current = true;
|
|
64
|
+
addUserMessage(initialTask);
|
|
65
|
+
void sendMessage(initialTask);
|
|
66
|
+
}
|
|
67
|
+
}, [hostReady, conversation, initialTask, messages.length, isRunning, addUserMessage, addAssistantMessage, sendMessage, resumeActive]);
|
|
68
|
+
const { executeCommand, isSlashCommand } = useSlashCommands({
|
|
69
|
+
onHelp: () => setMode('help'),
|
|
70
|
+
onSessions: async () => {
|
|
71
|
+
await loadSessions();
|
|
72
|
+
setMode('sessions');
|
|
73
|
+
},
|
|
74
|
+
onClear: () => clearMessages(),
|
|
75
|
+
onNew: async () => {
|
|
76
|
+
await newConversation();
|
|
77
|
+
},
|
|
78
|
+
onExit: () => exit(),
|
|
79
|
+
onQuit: () => exit(),
|
|
80
|
+
onCancel: async () => {
|
|
81
|
+
if (!isRunning) {
|
|
82
|
+
addAssistantMessage({
|
|
83
|
+
id: `cancel-${Date.now()}`,
|
|
84
|
+
role: 'system',
|
|
85
|
+
text: '当前没有正在运行的任务。',
|
|
86
|
+
createdAt: new Date().toISOString(),
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await cancelRun();
|
|
91
|
+
},
|
|
92
|
+
onConfig: () => {
|
|
93
|
+
addAssistantMessage({
|
|
94
|
+
id: `config-${Date.now()}`,
|
|
95
|
+
role: 'system',
|
|
96
|
+
text: JSON.stringify({
|
|
97
|
+
cwd: hostOptions.cwd ?? process.cwd(),
|
|
98
|
+
dataDir: hostOptions.dataDir ?? '默认 ~/.pulse/data',
|
|
99
|
+
provider: hostOptions.provider?.provider ?? 'mock',
|
|
100
|
+
model: hostOptions.provider?.defaultModel ?? '默认',
|
|
101
|
+
approvalMode: hostOptions.approvalMode ?? 'ask',
|
|
102
|
+
allowNetwork: hostOptions.allowNetwork ?? false,
|
|
103
|
+
}, null, 2),
|
|
104
|
+
createdAt: new Date().toISOString(),
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
onStatus: () => {
|
|
108
|
+
if (conversation) {
|
|
109
|
+
addAssistantMessage({
|
|
110
|
+
id: `status-${Date.now()}`,
|
|
111
|
+
role: 'system',
|
|
112
|
+
text: `会话 ID: ${conversation.id}\n工作区: ${conversation.summary.cwd}\n更新时间: ${conversation.summary.updatedAt}`,
|
|
113
|
+
createdAt: new Date().toISOString(),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
onTools: async () => {
|
|
118
|
+
if (host) {
|
|
119
|
+
try {
|
|
120
|
+
const doc = await host.doctor();
|
|
121
|
+
addAssistantMessage({
|
|
122
|
+
id: `tools-${Date.now()}`,
|
|
123
|
+
role: 'system',
|
|
124
|
+
text: `已注册工具 (${doc.tools.length}):\n${doc.tools.map((t) => `• ${t}`).join('\n')}`,
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
addAssistantMessage({
|
|
130
|
+
id: `tools-${Date.now()}`,
|
|
131
|
+
role: 'system',
|
|
132
|
+
text: `获取工具列表失败: ${String(e)}`,
|
|
133
|
+
createdAt: new Date().toISOString(),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
onArtifacts: async () => {
|
|
139
|
+
if (host && conversation) {
|
|
140
|
+
try {
|
|
141
|
+
const artifacts = await host.listArtifacts(conversation.id);
|
|
142
|
+
addAssistantMessage({
|
|
143
|
+
id: `artifacts-${Date.now()}`,
|
|
144
|
+
role: 'system',
|
|
145
|
+
text: artifacts.length === 0
|
|
146
|
+
? '暂无产物文件。'
|
|
147
|
+
: `产物列表 (${artifacts.length}):\n${artifacts.map((a) => `• ${a.path} (${a.bytes} bytes)`).join('\n')}`,
|
|
148
|
+
createdAt: new Date().toISOString(),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
addAssistantMessage({
|
|
153
|
+
id: `artifacts-${Date.now()}`,
|
|
154
|
+
role: 'system',
|
|
155
|
+
text: `获取产物列表失败: ${String(e)}`,
|
|
156
|
+
createdAt: new Date().toISOString(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
onDelete: async (id) => {
|
|
162
|
+
const targetId = id || conversation?.id;
|
|
163
|
+
if (targetId && host) {
|
|
164
|
+
try {
|
|
165
|
+
await host.deleteConversation(targetId);
|
|
166
|
+
if (targetId === conversation?.id) {
|
|
167
|
+
await newConversation();
|
|
168
|
+
}
|
|
169
|
+
addAssistantMessage({
|
|
170
|
+
id: `del-${Date.now()}`,
|
|
171
|
+
role: 'system',
|
|
172
|
+
text: `已删除会话: ${targetId}`,
|
|
173
|
+
createdAt: new Date().toISOString(),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
addAssistantMessage({
|
|
178
|
+
id: `del-${Date.now()}`,
|
|
179
|
+
role: 'system',
|
|
180
|
+
text: `删除失败: ${String(e)}`,
|
|
181
|
+
createdAt: new Date().toISOString(),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
onExport: async (format) => {
|
|
187
|
+
if (host && conversation) {
|
|
188
|
+
try {
|
|
189
|
+
const modeFormat = format === 'markdown' ? 'markdown' : 'json';
|
|
190
|
+
const content = await host.exportConversation(conversation.id, modeFormat);
|
|
191
|
+
addAssistantMessage({
|
|
192
|
+
id: `export-${Date.now()}`,
|
|
193
|
+
role: 'system',
|
|
194
|
+
text: `会话导出 (${modeFormat}):\n\n${content}`,
|
|
195
|
+
createdAt: new Date().toISOString(),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
addAssistantMessage({
|
|
200
|
+
id: `export-${Date.now()}`,
|
|
201
|
+
role: 'system',
|
|
202
|
+
text: `导出失败: ${String(e)}`,
|
|
203
|
+
createdAt: new Date().toISOString(),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
onModel: (name) => {
|
|
209
|
+
const model = name?.trim();
|
|
210
|
+
if (model && host)
|
|
211
|
+
host.setModel(model);
|
|
212
|
+
addAssistantMessage({
|
|
213
|
+
id: `model-${Date.now()}`,
|
|
214
|
+
role: 'system',
|
|
215
|
+
text: `当前模型设置为: ${model || host?.getModel() || hostOptions.provider?.defaultModel || '默认'}`,
|
|
216
|
+
createdAt: new Date().toISOString(),
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
onThinking: (arg) => {
|
|
220
|
+
const level = (arg || '').trim().toLowerCase();
|
|
221
|
+
let effort;
|
|
222
|
+
if (level === 'high' || level === 'medium' || level === 'low') {
|
|
223
|
+
effort = level;
|
|
224
|
+
}
|
|
225
|
+
else if (level === 'off' || level === 'none' || level === '0') {
|
|
226
|
+
effort = undefined;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
effort = host?.getReasoningEffort() === 'high' ? 'medium' : host?.getReasoningEffort() === 'medium' ? 'low' : 'high';
|
|
230
|
+
}
|
|
231
|
+
if (host) {
|
|
232
|
+
host.setReasoningEffort(effort);
|
|
233
|
+
}
|
|
234
|
+
setShowThinking(Boolean(effort));
|
|
235
|
+
addAssistantMessage({
|
|
236
|
+
id: `thinking-${Date.now()}`,
|
|
237
|
+
role: 'system',
|
|
238
|
+
text: `模型思考深度 (reasoning effort) 已设置为: ${effort || 'off (关闭)'}`,
|
|
239
|
+
createdAt: new Date().toISOString(),
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
onVerbose: () => {
|
|
243
|
+
setVerbosity('verbose');
|
|
244
|
+
addAssistantMessage({
|
|
245
|
+
id: `verbose-${Date.now()}`,
|
|
246
|
+
role: 'system',
|
|
247
|
+
text: '已切换为详细输出模式',
|
|
248
|
+
createdAt: new Date().toISOString(),
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
onQuiet: () => {
|
|
252
|
+
setVerbosity('quiet');
|
|
253
|
+
addAssistantMessage({
|
|
254
|
+
id: `quiet-${Date.now()}`,
|
|
255
|
+
role: 'system',
|
|
256
|
+
text: '已切换为精简输出模式',
|
|
257
|
+
createdAt: new Date().toISOString(),
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
onCompact: async () => {
|
|
261
|
+
if (!host || !conversation)
|
|
262
|
+
return;
|
|
263
|
+
addAssistantMessage({
|
|
264
|
+
id: `compact-start-${Date.now()}`,
|
|
265
|
+
role: 'system',
|
|
266
|
+
text: '正在调用模型对当前历史上下文进行提炼压缩...',
|
|
267
|
+
createdAt: new Date().toISOString(),
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
const res = await host.compactConversation(conversation.id);
|
|
271
|
+
await switchConversation(conversation.id);
|
|
272
|
+
addAssistantMessage({
|
|
273
|
+
id: `compact-done-${Date.now()}`,
|
|
274
|
+
role: 'system',
|
|
275
|
+
text: `模型上下文压缩完成。替换前的记录已备份为 messages.jsonl.bak。\n\n${res.text}`,
|
|
276
|
+
createdAt: new Date().toISOString(),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
addAssistantMessage({
|
|
281
|
+
id: `compact-err-${Date.now()}`,
|
|
282
|
+
role: 'system',
|
|
283
|
+
text: `模型压缩上下文失败: ${String(err)}`,
|
|
284
|
+
createdAt: new Date().toISOString(),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
const handleSubmit = useCallback(async (input) => {
|
|
290
|
+
const text = input.trim();
|
|
291
|
+
if (!text)
|
|
292
|
+
return;
|
|
293
|
+
if (isSlashCommand(text)) {
|
|
294
|
+
const handled = await executeCommand(text);
|
|
295
|
+
if (!handled) {
|
|
296
|
+
addAssistantMessage({
|
|
297
|
+
id: `command-${Date.now()}`,
|
|
298
|
+
role: 'system',
|
|
299
|
+
text: `未知命令:${text.split(/\s+/)[0] ?? text},输入 /help 查看可用命令。`,
|
|
300
|
+
createdAt: new Date().toISOString(),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
addUserMessage(text);
|
|
306
|
+
await sendMessage(text);
|
|
307
|
+
}
|
|
308
|
+
}, [isSlashCommand, executeCommand, addUserMessage, sendMessage]);
|
|
309
|
+
useInput((input, key) => {
|
|
310
|
+
if ((hostError || (conversationError && !conversation)) && (input === 'q' || key.escape)) {
|
|
311
|
+
exit();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (conversationError && !conversation && input === 'n') {
|
|
315
|
+
void newConversation();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (isRunning && key.escape) {
|
|
319
|
+
void cancelRun();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (mode === 'help') {
|
|
323
|
+
if (input === 'q' || key.escape || key.return) {
|
|
324
|
+
setMode('chat');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
if (hostError) {
|
|
329
|
+
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Pulse \u521D\u59CB\u5316\u5931\u8D25\uFF1A", hostError] }), _jsx(Text, { dimColor: true, children: "\u8BF7\u68C0\u67E5\u5DE5\u4F5C\u76EE\u5F55\u3001\u914D\u7F6E\u6587\u4EF6\u548C\u6570\u636E\u76EE\u5F55\u6743\u9650\u540E\u91CD\u8BD5\u3002" }), _jsx(Text, { dimColor: true, children: "\u6309 q \u9000\u51FA\u3002" })] }));
|
|
330
|
+
}
|
|
331
|
+
if (!hostReady || !conversation) {
|
|
332
|
+
if (conversationError) {
|
|
333
|
+
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["\u4F1A\u8BDD\u52A0\u8F7D\u5931\u8D25\uFF1A", conversationError] }), _jsx(Text, { dimColor: true, children: "\u6309 n \u65B0\u5EFA\u4F1A\u8BDD\uFF0C\u6309 q \u9000\u51FA\u3002" })] }));
|
|
334
|
+
}
|
|
335
|
+
return (_jsx(Box, { padding: 1, children: _jsx(Spinner, { label: "\u6B63\u5728\u521D\u59CB\u5316 Pulse \u8FD0\u884C\u65F6..." }) }));
|
|
336
|
+
}
|
|
337
|
+
if (mode === 'sessions') {
|
|
338
|
+
return (_jsx(SessionList, { sessions: sessionsList, notice: sessionError, onSelect: (id) => {
|
|
339
|
+
void switchConversation(id);
|
|
340
|
+
setMode('chat');
|
|
341
|
+
}, onDelete: async (id) => {
|
|
342
|
+
if (!host)
|
|
343
|
+
return;
|
|
344
|
+
try {
|
|
345
|
+
await host.deleteConversation(id);
|
|
346
|
+
if (id === conversation?.id)
|
|
347
|
+
await newConversation();
|
|
348
|
+
await loadSessions();
|
|
349
|
+
setSessionError(null);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
setSessionError(error instanceof Error ? error.message : String(error));
|
|
353
|
+
}
|
|
354
|
+
}, onBack: () => setMode('chat') }));
|
|
355
|
+
}
|
|
356
|
+
if (mode === 'help') {
|
|
357
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(HelpView, {}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u6309 Enter \u6216 q \u8FD4\u56DE\u5BF9\u8BDD\u6A21\u5F0F..." }) })] }));
|
|
358
|
+
}
|
|
359
|
+
const cwd = conversation.summary.cwd;
|
|
360
|
+
const title = conversation.summary.title;
|
|
361
|
+
const modelName = hostOptions.provider?.defaultModel || hostOptions.provider?.provider || 'default';
|
|
362
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(Header, { title: title, cwd: cwd, model: modelName, approvalMode: hostOptions.approvalMode ?? 'ask' }), messages.length === 0 && !isRunning && (_jsx(Welcome, { cwd: cwd, model: modelName, version: version })), messages.length > 0 && (_jsx(MessageList, { messages: messages, showThinking: showThinking, verbosity: verbosity })), isRunning && !approvalRequest && (_jsx(Box, { marginY: 1, children: _jsx(Spinner, { label: currentStep || '正在思考与执行...' }) })), runError && (_jsx(Box, { marginY: 1, children: _jsxs(Text, { color: "red", children: ["\u9519\u8BEF: ", runError] }) })), conversationError && (_jsx(Box, { marginY: 1, children: _jsxs(Text, { color: "red", children: ["\u4F1A\u8BDD\u6D88\u606F\u52A0\u8F7D\u5931\u8D25: ", conversationError] }) })), approvalRequest && (_jsx(ApprovalPrompt, { request: approvalRequest, onApprove: () => void approveAction(approvalRequest.effectId, true), onDeny: (reason) => void approveAction(approvalRequest.effectId, false, reason) })), _jsx(Box, { marginTop: 1, children: _jsx(InputArea, { onSubmit: (txt) => void handleSubmit(txt), disabled: false, placeholder: isRunning ? '运行中也可以输入;/cancel 可取消当前运行...' : '输入消息或 /help...' }) })] }));
|
|
363
|
+
}
|
|
364
|
+
export default App;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ApprovalRequest } from '../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
request: ApprovalRequest;
|
|
4
|
+
onApprove: () => void;
|
|
5
|
+
onDeny: (reason?: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function ApprovalPrompt({ request, onApprove, onDeny }: Props): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { theme } from '../theme.js';
|
|
4
|
+
import { describeApprovalTool } from '../utils/approval.js';
|
|
5
|
+
export function ApprovalPrompt({ request, onApprove, onDeny }) {
|
|
6
|
+
useInput((input) => {
|
|
7
|
+
if (input.length !== 1)
|
|
8
|
+
return;
|
|
9
|
+
const char = input.toLowerCase();
|
|
10
|
+
if (char === 'y') {
|
|
11
|
+
onApprove();
|
|
12
|
+
}
|
|
13
|
+
else if (char === 'n') {
|
|
14
|
+
onDeny('用户拒绝');
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
const tools = request.tools ?? [{ name: request.toolName, input: request.toolArgs }];
|
|
18
|
+
const previews = tools.map((tool) => describeApprovalTool(tool));
|
|
19
|
+
const truncated = previews.some((preview) => preview.truncated);
|
|
20
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: theme.warning, flexDirection: "column", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.warning, bold: true, children: ["\u9700\u8981\u6279\u51C6\uFF1A", request.toolName] }) }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: previews.map((preview, index) => (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsx(Text, { color: theme.tool, children: preview.name }), _jsx(Text, { children: preview.body })] }, `${preview.name}-${index}`))) }), truncated && (_jsx(Text, { color: theme.warning, children: "\u9884\u89C8\u5DF2\u622A\u65AD\u3002\u6309 Y \u4F1A\u6279\u51C6\u4E0A\u9762\u5217\u51FA\u7684\u5B8C\u6574\u64CD\u4F5C\uFF0C\u800C\u4E0D\u53EA\u662F\u53EF\u89C1\u7247\u6BB5\u3002" })), request.prompt && (_jsx(Box, { children: _jsx(Text, { children: request.prompt }) })), request.digest && _jsxs(Text, { color: theme.dim, children: ["\u6458\u8981\u6821\u9A8C\uFF1A", request.digest] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: "[Y] \u6279\u51C6 [N] \u62D2\u7EDD" }) })] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ToolCallDisplay, TokenStatsData, Verbosity } from '../types.js';
|
|
2
|
+
export interface AssistantMessageProps {
|
|
3
|
+
text: string;
|
|
4
|
+
thinking?: string | undefined;
|
|
5
|
+
showThinking?: boolean | undefined;
|
|
6
|
+
toolCalls?: ToolCallDisplay[] | undefined;
|
|
7
|
+
tokenStats?: TokenStatsData | undefined;
|
|
8
|
+
verbosity?: Verbosity | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare function AssistantMessage({ text, thinking, showThinking, toolCalls, tokenStats, verbosity, }: AssistantMessageProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { ThinkingBlock } from './ThinkingBlock.js';
|
|
4
|
+
import { ToolCallCard } from './ToolCallCard.js';
|
|
5
|
+
import { TokenStats } from './TokenStats.js';
|
|
6
|
+
import { renderMarkdownToAnsi } from '../utils/markdown.js';
|
|
7
|
+
export function AssistantMessage({ text, thinking, showThinking, toolCalls, tokenStats, verbosity = 'normal', }) {
|
|
8
|
+
const renderedText = text ? renderMarkdownToAnsi(text) : '';
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [showThinking && thinking && (_jsx(ThinkingBlock, { content: thinking, visible: true })), verbosity !== 'quiet' && toolCalls && toolCalls.length > 0 && (_jsx(Box, { flexDirection: "column", gap: 1, marginY: 1, children: toolCalls.map((tc, idx) => (_jsx(ToolCallCard, { call: tc }, idx))) })), renderedText && (_jsx(Box, { children: _jsx(Text, { children: renderedText }) })), verbosity === 'verbose' && tokenStats && (_jsx(TokenStats, { inputTokens: tokenStats.inputTokens, outputTokens: tokenStats.outputTokens, durationMs: tokenStats.durationMs, estimatedCost: tokenStats.estimatedCost }))] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { theme } from '../theme.js';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
export function Header({ title, cwd, model, approvalMode = 'ask' }) {
|
|
6
|
+
const cwdBasename = path.basename(cwd);
|
|
7
|
+
return (_jsxs(Box, { borderBottom: true, borderStyle: "single", borderColor: theme.border, borderTop: false, borderLeft: false, borderRight: false, width: "100%", children: [_jsx(Text, { color: theme.primary, bold: true, children: "Pulse" }), _jsx(Text, { color: theme.dim, children: " \u00B7 " }), _jsx(Text, { color: "white", children: title }), _jsx(Text, { color: theme.dim, children: " \u00B7 " }), _jsx(Text, { color: theme.dim, children: model }), _jsx(Text, { color: theme.dim, children: " \u00B7 " }), _jsx(Text, { color: approvalMode === 'auto' ? theme.warning : theme.dim, children: approvalMode === 'auto' ? '自动批准' : approvalMode === 'read-only' ? '只读' : '需审批' }), _jsx(Text, { color: theme.dim, children: " \u00B7 " }), _jsx(Text, { color: theme.dim, children: cwdBasename })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function HelpView(): import("react").JSX.Element;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { theme } from '../theme.js';
|
|
4
|
+
const COMMANDS = [
|
|
5
|
+
{
|
|
6
|
+
category: 'General',
|
|
7
|
+
commands: [
|
|
8
|
+
{ name: '/help', desc: '显示帮助信息' },
|
|
9
|
+
{ name: '/status', desc: '查看会话状态' },
|
|
10
|
+
{ name: '/tools', desc: '列出可用工具' },
|
|
11
|
+
{ name: '/artifacts', desc: '查看当前产物列表' },
|
|
12
|
+
{ name: '/exit, /quit', desc: '退出 CLI' },
|
|
13
|
+
{ name: '/cancel, /stop', desc: '取消当前运行并保留会话' },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
category: 'Session',
|
|
18
|
+
commands: [
|
|
19
|
+
{ name: '/new', desc: '新建会话' },
|
|
20
|
+
{ name: '/sessions', desc: '交互式历史会话管理' },
|
|
21
|
+
{ name: '/delete [id]', desc: '删除指定历史会话' },
|
|
22
|
+
{ name: '/export [format]', desc: '导出会话 (markdown / json)' },
|
|
23
|
+
{ name: '/clear', desc: '清屏当前消息' },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
category: 'Display',
|
|
28
|
+
commands: [
|
|
29
|
+
{ name: '/verbose, /quiet', desc: '切换输出详细程度' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
category: 'Model & Context',
|
|
34
|
+
commands: [
|
|
35
|
+
{ name: '/model [name]', desc: '切换模型' },
|
|
36
|
+
{ name: '/thinking [level]', desc: '设置模型思考深度 (low/medium/high/off)' },
|
|
37
|
+
{ name: '/compact', desc: '用已配置模型压缩历史,并备份原记录' },
|
|
38
|
+
{ name: '/config', desc: '查看当前运行时配置' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
export function HelpView() {
|
|
43
|
+
return (_jsx(Box, { flexDirection: "column", marginY: 1, children: COMMANDS.map((section, idx) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: theme.primary, bold: true, children: section.category }), section.commands.map((cmd, cIdx) => (_jsxs(Box, { children: [_jsx(Box, { width: 28, children: _jsx(Text, { color: theme.accent, children: cmd.name }) }), _jsx(Text, { children: cmd.desc })] }, cIdx)))] }, idx))) }));
|
|
44
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { theme } from '../theme.js';
|
|
6
|
+
export function InputArea({ onSubmit, disabled, placeholder }) {
|
|
7
|
+
const [value, setValue] = useState('');
|
|
8
|
+
const [accumulatedLines, setAccumulatedLines] = useState([]);
|
|
9
|
+
const [history, setHistory] = useState([]);
|
|
10
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
11
|
+
useInput((input, key) => {
|
|
12
|
+
if (disabled)
|
|
13
|
+
return;
|
|
14
|
+
if (key.upArrow) {
|
|
15
|
+
if (historyIndex < history.length - 1) {
|
|
16
|
+
const nextIndex = historyIndex + 1;
|
|
17
|
+
setHistoryIndex(nextIndex);
|
|
18
|
+
setValue(history[history.length - 1 - nextIndex] || '');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (key.downArrow) {
|
|
22
|
+
if (historyIndex > 0) {
|
|
23
|
+
const nextIndex = historyIndex - 1;
|
|
24
|
+
setHistoryIndex(nextIndex);
|
|
25
|
+
setValue(history[history.length - 1 - nextIndex] || '');
|
|
26
|
+
}
|
|
27
|
+
else if (historyIndex === 0) {
|
|
28
|
+
setHistoryIndex(-1);
|
|
29
|
+
setValue('');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const handleSubmit = (text) => {
|
|
34
|
+
if (text.endsWith('\\')) {
|
|
35
|
+
const newLine = text.slice(0, -1);
|
|
36
|
+
setAccumulatedLines([...accumulatedLines, newLine]);
|
|
37
|
+
setValue('');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const finalLines = [...accumulatedLines, text];
|
|
41
|
+
const fullText = finalLines.join('\n');
|
|
42
|
+
if (fullText.trim()) {
|
|
43
|
+
const newHistory = [...history, fullText].slice(-50);
|
|
44
|
+
setHistory(newHistory);
|
|
45
|
+
}
|
|
46
|
+
setHistoryIndex(-1);
|
|
47
|
+
setAccumulatedLines([]);
|
|
48
|
+
setValue('');
|
|
49
|
+
onSubmit(fullText);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const promptChar = accumulatedLines.length > 0 ? '…' : '›';
|
|
53
|
+
const promptColor = disabled ? theme.dim : theme.primary;
|
|
54
|
+
return (_jsxs(Box, { flexDirection: "column", children: [accumulatedLines.map((line, idx) => (_jsxs(Box, { children: [_jsxs(Text, { color: promptColor, children: [promptChar, " "] }), _jsx(Text, { children: line })] }, idx))), _jsxs(Box, { children: [_jsxs(Text, { color: promptColor, children: [promptChar, " "] }), disabled ? (_jsx(Text, { color: theme.dim, children: placeholder || '处理中...' })) : (_jsx(TextInput, { value: value, onChange: setValue, onSubmit: handleSubmit, placeholder: placeholder || '' }))] })] }));
|
|
55
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DisplayMessage, Verbosity } from '../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
messages: DisplayMessage[];
|
|
4
|
+
showThinking?: boolean | undefined;
|
|
5
|
+
verbosity?: Verbosity | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare function MessageList({ messages, showThinking, verbosity }: Props): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { theme } from '../theme.js';
|
|
4
|
+
import { UserMessage } from './UserMessage.js';
|
|
5
|
+
import { AssistantMessage } from './AssistantMessage.js';
|
|
6
|
+
import { stripTerminalControls } from '../utils/ansi.js';
|
|
7
|
+
export function MessageList({ messages, showThinking, verbosity = 'normal' }) {
|
|
8
|
+
return (_jsx(Box, { flexDirection: "column", children: messages.map((msg, index) => {
|
|
9
|
+
const isLast = index === messages.length - 1;
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", children: [msg.role === 'user' && (_jsx(UserMessage, { text: msg.text, timestamp: msg.createdAt })), msg.role === 'assistant' && (_jsx(AssistantMessage, { text: msg.text, thinking: msg.thinking, showThinking: showThinking, verbosity: verbosity, toolCalls: msg.toolCalls, tokenStats: msg.tokenStats })), msg.role === 'system' && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.dim, italic: true, children: stripTerminalControls(msg.text) }) })), !isLast && _jsx(Box, { height: 1 })] }, msg.id || index));
|
|
11
|
+
}) }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface Session {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
updatedAt: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
}
|
|
7
|
+
interface Props {
|
|
8
|
+
sessions: Session[];
|
|
9
|
+
notice?: string | null;
|
|
10
|
+
onSelect: (id: string) => void;
|
|
11
|
+
onDelete?: (id: string) => void | Promise<void>;
|
|
12
|
+
onBack: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function SessionList({ sessions, notice, onSelect, onDelete, onBack }: Props): import("react").JSX.Element;
|
|
15
|
+
export {};
|