@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.
Files changed (71) hide show
  1. package/dist/bin.d.ts +10 -1
  2. package/dist/bin.js +86 -177
  3. package/dist/commands/doctor.d.ts +2 -0
  4. package/dist/commands/doctor.js +36 -0
  5. package/dist/commands/interactive.d.ts +2 -0
  6. package/dist/commands/interactive.js +27 -0
  7. package/dist/commands/resume.d.ts +2 -0
  8. package/dist/commands/resume.js +75 -0
  9. package/dist/commands/run.d.ts +2 -0
  10. package/dist/commands/run.js +73 -0
  11. package/dist/commands/sessions.d.ts +2 -0
  12. package/dist/commands/sessions.js +66 -0
  13. package/dist/commands/setup.d.ts +1 -0
  14. package/dist/commands/setup.js +30 -0
  15. package/dist/commands/signals.d.ts +2 -0
  16. package/dist/commands/signals.js +20 -0
  17. package/dist/components/App.d.ts +10 -0
  18. package/dist/components/App.js +364 -0
  19. package/dist/components/ApprovalPrompt.d.ts +8 -0
  20. package/dist/components/ApprovalPrompt.js +21 -0
  21. package/dist/components/AssistantMessage.d.ts +10 -0
  22. package/dist/components/AssistantMessage.js +10 -0
  23. package/dist/components/Header.d.ts +8 -0
  24. package/dist/components/Header.js +8 -0
  25. package/dist/components/HelpView.d.ts +1 -0
  26. package/dist/components/HelpView.js +44 -0
  27. package/dist/components/InputArea.d.ts +7 -0
  28. package/dist/components/InputArea.js +55 -0
  29. package/dist/components/MessageList.d.ts +8 -0
  30. package/dist/components/MessageList.js +12 -0
  31. package/dist/components/SessionList.d.ts +15 -0
  32. package/dist/components/SessionList.js +33 -0
  33. package/dist/components/Spinner.d.ts +4 -0
  34. package/dist/components/Spinner.js +7 -0
  35. package/dist/components/ThinkingBlock.d.ts +5 -0
  36. package/dist/components/ThinkingBlock.js +8 -0
  37. package/dist/components/TokenStats.d.ts +7 -0
  38. package/dist/components/TokenStats.js +15 -0
  39. package/dist/components/ToolCallCard.d.ts +8 -0
  40. package/dist/components/ToolCallCard.js +31 -0
  41. package/dist/components/UserMessage.d.ts +5 -0
  42. package/dist/components/UserMessage.js +7 -0
  43. package/dist/components/Welcome.d.ts +6 -0
  44. package/dist/components/Welcome.js +6 -0
  45. package/dist/hooks/useConversation.d.ts +15 -0
  46. package/dist/hooks/useConversation.js +115 -0
  47. package/dist/hooks/useHost.d.ts +7 -0
  48. package/dist/hooks/useHost.js +44 -0
  49. package/dist/hooks/useMultilineInput.d.ts +11 -0
  50. package/dist/hooks/useMultilineInput.js +58 -0
  51. package/dist/hooks/useRun.d.ts +17 -0
  52. package/dist/hooks/useRun.js +213 -0
  53. package/dist/hooks/useSlashCommands.d.ts +26 -0
  54. package/dist/hooks/useSlashCommands.js +45 -0
  55. package/dist/hooks/useTokenStats.d.ts +7 -0
  56. package/dist/hooks/useTokenStats.js +38 -0
  57. package/dist/theme.d.ts +27 -0
  58. package/dist/theme.js +27 -0
  59. package/dist/types.d.ts +65 -0
  60. package/dist/types.js +1 -0
  61. package/dist/utils/ansi.d.ts +5 -0
  62. package/dist/utils/ansi.js +20 -0
  63. package/dist/utils/approval.d.ts +10 -0
  64. package/dist/utils/approval.js +50 -0
  65. package/dist/utils/format.d.ts +6 -0
  66. package/dist/utils/format.js +51 -0
  67. package/dist/utils/highlight.d.ts +1 -0
  68. package/dist/utils/highlight.js +80 -0
  69. package/dist/utils/markdown.d.ts +1 -0
  70. package/dist/utils/markdown.js +138 -0
  71. 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,2 @@
1
+ import type { LocalHost, RunHandle } from '@hunterzhu/pulse-server';
2
+ export declare function bindRunSignals(host: LocalHost, getRun: () => RunHandle | undefined): () => void;
@@ -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
+ interface Props {
2
+ title: string;
3
+ cwd: string;
4
+ model: string;
5
+ approvalMode?: 'read-only' | 'ask' | 'auto' | undefined;
6
+ }
7
+ export declare function Header({ title, cwd, model, approvalMode }: Props): import("react").JSX.Element;
8
+ export {};
@@ -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,7 @@
1
+ interface Props {
2
+ onSubmit: (text: string) => void;
3
+ disabled?: boolean;
4
+ placeholder?: string;
5
+ }
6
+ export declare function InputArea({ onSubmit, disabled, placeholder }: Props): import("react").JSX.Element;
7
+ export {};
@@ -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 {};