@hunterzhu/pulse-cli 0.2.1 → 0.4.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.
- package/README.md +1 -1
- package/dist/bin.js +0 -5
- package/dist/commands/interactive.js +11 -5
- package/dist/components/App.js +56 -8
- package/dist/components/HelpView.js +2 -1
- package/dist/components/InputArea.d.ts +3 -1
- package/dist/components/InputArea.js +13 -20
- package/dist/components/MessageList.d.ts +2 -1
- package/dist/components/MessageList.js +2 -2
- package/dist/components/MessageViewport.d.ts +2 -1
- package/dist/components/MessageViewport.js +63 -3
- package/dist/components/StatusHud.d.ts +1 -2
- package/dist/components/StatusHud.js +2 -2
- package/dist/config.d.ts +0 -2
- package/dist/config.js +0 -1
- package/dist/hooks/useConversation.d.ts +3 -1
- package/dist/hooks/useConversation.js +3 -1
- package/dist/hooks/useRun.d.ts +3 -1
- package/dist/hooks/useRun.js +138 -39
- package/dist/hooks/useSlashCommands.d.ts +1 -1
- package/dist/hooks/useSlashCommands.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/utils/clipboard.d.ts +14 -0
- package/dist/utils/clipboard.js +49 -0
- package/dist/utils/inputHistory.d.ts +11 -0
- package/dist/utils/inputHistory.js +24 -0
- package/dist/utils/mouse.d.ts +10 -5
- package/dist/utils/mouse.js +14 -4
- package/dist/utils/selection.d.ts +13 -0
- package/dist/utils/selection.js +107 -0
- package/dist/utils/transcript.js +43 -10
- package/package.json +2 -2
- package/scripts/postinstall.mjs +50 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hunterzhu/pulse-cli
|
|
2
2
|
|
|
3
|
-
**A
|
|
3
|
+
**A neural-signal like AI assistant for coding, research and file tasks.**
|
|
4
4
|
|
|
5
5
|
Pulse runs in your project folder, keeps conversations on your machine, and can resume unfinished work. Requires Node.js 22+.
|
|
6
6
|
|
package/dist/bin.js
CHANGED
|
@@ -46,7 +46,6 @@ Options:
|
|
|
46
46
|
--max-output-tokens <n> maximum generated tokens (default 4096)
|
|
47
47
|
--reasoning-effort <x> low, medium, or high
|
|
48
48
|
--max-turns <n> maximum ReAct model/tool turns (default 32)
|
|
49
|
-
--execution-mode <mode> serial (default) or parallel-read (up to three read-only lanes)
|
|
50
49
|
--auto-compact-percent <n> compact automatically at this percent of the context window (1-90, default 90)
|
|
51
50
|
--format <text|jsonl> output format
|
|
52
51
|
--resume enter the latest saved session
|
|
@@ -234,9 +233,6 @@ export async function hostOptions(parsed) {
|
|
|
234
233
|
? 'auto'
|
|
235
234
|
: requestedApprovalMode === 'ask' ? 'ask' : config.approvalMode;
|
|
236
235
|
const allowNetwork = parsed.options['no-network'] === true ? false : parsed.options['allow-network'] === true || process.env.PULSE_ALLOW_NETWORK === '1' ? true : config.allowNetwork;
|
|
237
|
-
const executionMode = option(parsed.options, 'execution-mode') ?? process.env.PULSE_EXECUTION_MODE ?? config.executionMode ?? 'serial';
|
|
238
|
-
if (executionMode !== 'serial' && executionMode !== 'parallel-read')
|
|
239
|
-
throw new Error('INVALID_EXECUTION_MODE: expected serial or parallel-read');
|
|
240
236
|
const capabilitySettings = config.capabilities ?? {};
|
|
241
237
|
const skillRoots = capabilitySettings.trustedSkillRoots ?? [];
|
|
242
238
|
if (!Array.isArray(skillRoots) || skillRoots.some((root) => typeof root !== 'string' || !isAbsolute(expandHome(root) ?? root)))
|
|
@@ -315,7 +311,6 @@ export async function hostOptions(parsed) {
|
|
|
315
311
|
...(configuredAutoCompactPercent === undefined ? {} : { autoCompactPercent: Math.min(90, configuredAutoCompactPercent) }),
|
|
316
312
|
...(allowNetwork === undefined ? {} : { allowNetwork }),
|
|
317
313
|
...(config.networkHosts === undefined ? {} : { networkHosts: config.networkHosts }),
|
|
318
|
-
executionMode,
|
|
319
314
|
};
|
|
320
315
|
}
|
|
321
316
|
export async function main() {
|
|
@@ -22,14 +22,20 @@ export async function runInteractive(options, conversationId, initialTask, versi
|
|
|
22
22
|
// Keep Pulse in its own full-screen buffer so the shell's previous
|
|
23
23
|
// command history does not become part of the chat workspace.
|
|
24
24
|
alternateScreen: true,
|
|
25
|
+
// Ctrl+C is a selection-copy shortcut inside the chat. Exit explicitly
|
|
26
|
+
// through /exit so an accidental keypress cannot drop an active task.
|
|
27
|
+
exitOnCtrlC: false,
|
|
25
28
|
});
|
|
26
29
|
let interrupted = false;
|
|
27
|
-
const
|
|
30
|
+
const onInterrupt = () => {
|
|
31
|
+
// Ignore SIGINT while the interactive app is active; /exit is the exit path.
|
|
32
|
+
};
|
|
33
|
+
const onTerminate = () => {
|
|
28
34
|
interrupted = true;
|
|
29
35
|
unmount();
|
|
30
36
|
};
|
|
31
|
-
process.on('SIGINT',
|
|
32
|
-
process.on('SIGTERM',
|
|
37
|
+
process.on('SIGINT', onInterrupt);
|
|
38
|
+
process.on('SIGTERM', onTerminate);
|
|
33
39
|
try {
|
|
34
40
|
await waitUntilExit();
|
|
35
41
|
return interrupted ? 130 : 0;
|
|
@@ -39,8 +45,8 @@ export async function runInteractive(options, conversationId, initialTask, versi
|
|
|
39
45
|
return 1;
|
|
40
46
|
}
|
|
41
47
|
finally {
|
|
42
|
-
process.off('SIGINT',
|
|
43
|
-
process.off('SIGTERM',
|
|
48
|
+
process.off('SIGINT', onInterrupt);
|
|
49
|
+
process.off('SIGTERM', onTerminate);
|
|
44
50
|
await closePendingHosts();
|
|
45
51
|
}
|
|
46
52
|
}
|
package/dist/components/App.js
CHANGED
|
@@ -16,6 +16,8 @@ import { useConversation } from '../hooks/useConversation.js';
|
|
|
16
16
|
import { useRun } from '../hooks/useRun.js';
|
|
17
17
|
import { useTokenStats } from '../hooks/useTokenStats.js';
|
|
18
18
|
import { useSlashCommands } from '../hooks/useSlashCommands.js';
|
|
19
|
+
import { copyToClipboard, isCopyShortcut } from '../utils/clipboard.js';
|
|
20
|
+
import { emptyInputHistory, navigateInputHistory, rememberInput } from '../utils/inputHistory.js';
|
|
19
21
|
export function App({ hostOptions, conversationId: initialConversationId, initialTask, resumeOnStart = false, version, }) {
|
|
20
22
|
const { exit } = useApp();
|
|
21
23
|
const { stdout } = useStdout();
|
|
@@ -23,6 +25,10 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
23
25
|
useEffect(() => { const resize = () => setTerminalRows(stdout.rows || 24); stdout.on('resize', resize); return () => { stdout.off('resize', resize); }; }, [stdout]);
|
|
24
26
|
const [mode, setMode] = useState('chat');
|
|
25
27
|
const [mouseEnabled, setMouseEnabled] = useState(true);
|
|
28
|
+
const [selectedText, setSelectedText] = useState('');
|
|
29
|
+
const [inputHistory, setInputHistory] = useState(emptyInputHistory);
|
|
30
|
+
const inputHistoryRef = useRef(inputHistory);
|
|
31
|
+
inputHistoryRef.current = inputHistory;
|
|
26
32
|
const [showThinking, setShowThinking] = useState(false);
|
|
27
33
|
const [verbosity, setVerbosity] = useState('normal');
|
|
28
34
|
const [sessionsList, setSessionsList] = useState([]);
|
|
@@ -33,12 +39,35 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
33
39
|
const startedResume = useRef(false);
|
|
34
40
|
const { host, error: hostError, ready: hostReady } = useHost(hostOptions);
|
|
35
41
|
const { conversation, messages, addUserMessage, addAssistantMessage, clearMessages, switchConversation, newConversation, createConversation, error: conversationError, } = useConversation({ host, conversationId: initialConversationId });
|
|
36
|
-
const { isRunning,
|
|
42
|
+
const { isRunning, error: runError, approvalRequest, askRequest, lanes, approvalSubmitting, sendMessage, resumeActive, approveAction, replyAsk, cancelRun, } = useRun({
|
|
37
43
|
host,
|
|
38
44
|
conversationId: conversation?.id ?? null,
|
|
39
45
|
addAssistantMessage,
|
|
40
46
|
});
|
|
41
47
|
const { cumulative } = useTokenStats();
|
|
48
|
+
const rememberUserInput = useCallback((text) => {
|
|
49
|
+
const next = rememberInput(inputHistoryRef.current, text);
|
|
50
|
+
inputHistoryRef.current = next;
|
|
51
|
+
setInputHistory(next);
|
|
52
|
+
}, []);
|
|
53
|
+
const navigateUserInputHistory = useCallback((direction, currentValue) => {
|
|
54
|
+
const result = navigateInputHistory(inputHistoryRef.current, direction, currentValue);
|
|
55
|
+
inputHistoryRef.current = result.state;
|
|
56
|
+
setInputHistory(result.state);
|
|
57
|
+
return result.value;
|
|
58
|
+
}, []);
|
|
59
|
+
useEffect(() => { setSelectedText(''); }, [conversation?.id]);
|
|
60
|
+
const copySelectedText = useCallback(async () => {
|
|
61
|
+
if (!selectedText)
|
|
62
|
+
return;
|
|
63
|
+
try {
|
|
64
|
+
await copyToClipboard(selectedText);
|
|
65
|
+
addAssistantMessage({ id: `copy-${Date.now()}`, role: 'system', text: `已复制选中的内容(${selectedText.length.toLocaleString()} 个字符)。`, createdAt: new Date().toISOString() });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
addAssistantMessage({ id: `copy-${Date.now()}`, role: 'system', text: '复制失败:当前系统没有可用的剪贴板工具。', createdAt: new Date().toISOString() });
|
|
69
|
+
}
|
|
70
|
+
}, [selectedText, addAssistantMessage]);
|
|
42
71
|
useEffect(() => {
|
|
43
72
|
if (approvalRequest) {
|
|
44
73
|
setApprovalChoiceFocused(true);
|
|
@@ -100,6 +129,25 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
100
129
|
}, [hostReady, conversation, initialTask, isRunning, addUserMessage, addAssistantMessage, createConversation, sendMessage]);
|
|
101
130
|
const { executeCommand, isSlashCommand } = useSlashCommands({
|
|
102
131
|
onMouse: (arg) => setMouseEnabled((current) => arg === 'off' ? false : arg === 'on' ? true : !current),
|
|
132
|
+
onCopy: async () => {
|
|
133
|
+
if (selectedText) {
|
|
134
|
+
await copySelectedText();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const latest = [...messages].reverse().find((message) => message.role === 'assistant' && message.text.trim() && message.streamStatus !== 'streaming' && message.streamStatus !== 'incomplete');
|
|
138
|
+
const payload = latest?.text || '';
|
|
139
|
+
if (!payload) {
|
|
140
|
+
addAssistantMessage({ id: `copy-${Date.now()}`, role: 'system', text: '请先拖动选择内容,或发送一条消息后再复制回复。', createdAt: new Date().toISOString() });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
await copyToClipboard(payload);
|
|
145
|
+
addAssistantMessage({ id: `copy-${Date.now()}`, role: 'system', text: `已复制最近一条完整回复(${payload.length.toLocaleString()} 个字符)。`, createdAt: new Date().toISOString() });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
addAssistantMessage({ id: `copy-${Date.now()}`, role: 'system', text: '复制失败:当前系统没有可用的剪贴板工具。可试试 /mouse off 后用终端选择文本。', createdAt: new Date().toISOString() });
|
|
149
|
+
}
|
|
150
|
+
},
|
|
103
151
|
onHelp: () => setMode('help'),
|
|
104
152
|
onSessions: async () => {
|
|
105
153
|
await loadSessions();
|
|
@@ -156,7 +204,6 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
156
204
|
}
|
|
157
205
|
},
|
|
158
206
|
onExit: () => exit(),
|
|
159
|
-
onQuit: () => exit(),
|
|
160
207
|
onCancel: async () => {
|
|
161
208
|
if (!isRunning) {
|
|
162
209
|
addAssistantMessage({
|
|
@@ -425,8 +472,9 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
425
472
|
}
|
|
426
473
|
}, [isSlashCommand, executeCommand, conversation, createConversation, addUserMessage, sendMessage]);
|
|
427
474
|
useInput((input, key) => {
|
|
428
|
-
if ((
|
|
429
|
-
|
|
475
|
+
if (isCopyShortcut(input, key)) {
|
|
476
|
+
if (selectedText)
|
|
477
|
+
void copySelectedText();
|
|
430
478
|
return;
|
|
431
479
|
}
|
|
432
480
|
if (conversationError && !conversation && input === 'n') {
|
|
@@ -452,13 +500,13 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
452
500
|
}
|
|
453
501
|
});
|
|
454
502
|
if (hostError) {
|
|
455
|
-
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: "\
|
|
503
|
+
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: "\u8F93\u5165 /exit \u9000\u51FA\u3002" })] }));
|
|
456
504
|
}
|
|
457
505
|
if (!hostReady) {
|
|
458
506
|
return (_jsx(Box, { padding: 1, children: _jsx(Spinner, { label: "\u6B63\u5728\u521D\u59CB\u5316 Pulse \u8FD0\u884C\u65F6..." }) }));
|
|
459
507
|
}
|
|
460
508
|
if (conversationError && !conversation) {
|
|
461
|
-
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\
|
|
509
|
+
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\u8F93\u5165 /exit \u9000\u51FA\u3002" })] }));
|
|
462
510
|
}
|
|
463
511
|
if (mode === 'sessions') {
|
|
464
512
|
return (_jsx(SessionList, { sessions: sessionsList, notice: sessionError, onSelect: (id) => {
|
|
@@ -486,13 +534,13 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
486
534
|
const title = conversation?.summary.title ?? 'New conversation';
|
|
487
535
|
const modelName = host?.getModel() || hostOptions.provider?.defaultModel || hostOptions.provider?.provider || 'default';
|
|
488
536
|
const providerName = host?.getProvider() || hostOptions.activeProviderCode || hostOptions.provider?.provider || 'default';
|
|
489
|
-
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: terminalRows, 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, { mouseEnabled: mouseEnabled, messages: messages, showThinking: showThinking, verbosity: verbosity, isRunning: isRunning })), 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] }) })), _jsx(StatusHud, { isRunning: isRunning, cwd: cwd, model: modelName, provider: providerName, approvalMode: hostOptions.approvalMode ?? 'ask',
|
|
537
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: terminalRows, 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, { mouseEnabled: mouseEnabled, messages: messages, showThinking: showThinking, verbosity: verbosity, isRunning: isRunning, onSelectionChange: setSelectedText })), 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] }) })), _jsx(StatusHud, { isRunning: isRunning, cwd: cwd, model: modelName, provider: providerName, approvalMode: hostOptions.approvalMode ?? 'ask', lanes: lanes }), approvalRequest ? (_jsx(ApprovalPrompt, { request: approvalRequest, inputMode: approvalInputMode, isFocused: approvalChoiceFocused && !approvalSubmitting, onApprove: () => {
|
|
490
538
|
void approveAction(approvalRequest.effectId, true);
|
|
491
539
|
}, onDeny: (reason) => {
|
|
492
540
|
void approveAction(approvalRequest.effectId, false, reason);
|
|
493
541
|
}, onInput: () => {
|
|
494
542
|
setApprovalChoiceFocused(false);
|
|
495
543
|
setApprovalInputMode(true);
|
|
496
|
-
} })) : askRequest ? (_jsx(AskPrompt, { request: askRequest, disabled: approvalSubmitting, onReply: (value) => void replyAsk(askRequest.effectId, value) }, askRequest.effectId)) : (_jsx(Box, { marginTop: 1, children: _jsx(InputArea, { onSubmit: (txt) => void handleSubmit(txt), disabled: approvalSubmitting, focus: !approvalSubmitting, placeholder: isRunning ? '运行中也可以输入;/cancel 可取消当前运行...' : '输入消息或 /help...' }) }))] }));
|
|
544
|
+
} })) : askRequest ? (_jsx(AskPrompt, { request: askRequest, disabled: approvalSubmitting, onReply: (value) => void replyAsk(askRequest.effectId, value) }, askRequest.effectId)) : (_jsx(Box, { marginTop: 1, children: _jsx(InputArea, { onSubmit: (txt) => void handleSubmit(txt), onRememberInput: rememberUserInput, onNavigateHistory: navigateUserInputHistory, disabled: approvalSubmitting, focus: !approvalSubmitting, placeholder: isRunning ? '运行中也可以输入;/cancel 可取消当前运行...' : '输入消息或 /help...' }) }))] }));
|
|
497
545
|
}
|
|
498
546
|
export default App;
|
|
@@ -6,11 +6,12 @@ const COMMANDS = [
|
|
|
6
6
|
category: 'General',
|
|
7
7
|
commands: [
|
|
8
8
|
{ name: '/mouse on|off', desc: '开关滚轮/触控板滚动;关闭后可原生选择文本' },
|
|
9
|
+
{ name: '/copy', desc: '复制当前选区或最近一条完整回复' },
|
|
9
10
|
{ name: '/help', desc: '显示帮助信息' },
|
|
10
11
|
{ name: '/status', desc: '查看会话状态' },
|
|
11
12
|
{ name: '/tools', desc: '列出可用工具' },
|
|
12
13
|
{ name: '/artifacts', desc: '查看当前产物列表' },
|
|
13
|
-
{ name: '/exit
|
|
14
|
+
{ name: '/exit', desc: '退出 CLI' },
|
|
14
15
|
{ name: '/cancel, /stop', desc: '取消当前运行并保留会话' },
|
|
15
16
|
],
|
|
16
17
|
},
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
onSubmit: (text: string) => void;
|
|
3
|
+
onRememberInput: (text: string) => void;
|
|
4
|
+
onNavigateHistory: (direction: 'up' | 'down', currentValue: string) => string | undefined;
|
|
3
5
|
disabled?: boolean;
|
|
4
6
|
focus?: boolean;
|
|
5
7
|
placeholder?: string;
|
|
6
8
|
}
|
|
7
|
-
export declare function InputArea({ onSubmit, disabled, focus, placeholder }: Props): import("react").JSX.Element;
|
|
9
|
+
export declare function InputArea({ onSubmit, onRememberInput, onNavigateHistory, disabled, focus, placeholder }: Props): import("react").JSX.Element;
|
|
8
10
|
export {};
|
|
@@ -3,11 +3,9 @@ import { Box, Text, useInput } from 'ink';
|
|
|
3
3
|
import TextInput from './TextInput.js';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { theme } from '../theme.js';
|
|
6
|
-
export function InputArea({ onSubmit, disabled, focus = true, placeholder }) {
|
|
6
|
+
export function InputArea({ onSubmit, onRememberInput, onNavigateHistory, disabled, focus = true, placeholder }) {
|
|
7
7
|
const [value, setValue] = useState('');
|
|
8
8
|
const [accumulatedLines, setAccumulatedLines] = useState([]);
|
|
9
|
-
const [history, setHistory] = useState([]);
|
|
10
|
-
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
11
9
|
useInput((input, key) => {
|
|
12
10
|
if (disabled || !focus)
|
|
13
11
|
return;
|
|
@@ -24,24 +22,21 @@ export function InputArea({ onSubmit, disabled, focus = true, placeholder }) {
|
|
|
24
22
|
return;
|
|
25
23
|
}
|
|
26
24
|
if (key.upArrow) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
setValue(history[history.length - 1 - nextIndex] || '');
|
|
31
|
-
}
|
|
25
|
+
const previous = onNavigateHistory('up', [...accumulatedLines, value].join('\n'));
|
|
26
|
+
if (previous !== undefined)
|
|
27
|
+
restoreInput(previous);
|
|
32
28
|
}
|
|
33
29
|
else if (key.downArrow) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
setValue(history[history.length - 1 - nextIndex] || '');
|
|
38
|
-
}
|
|
39
|
-
else if (historyIndex === 0) {
|
|
40
|
-
setHistoryIndex(-1);
|
|
41
|
-
setValue('');
|
|
42
|
-
}
|
|
30
|
+
const next = onNavigateHistory('down', [...accumulatedLines, value].join('\n'));
|
|
31
|
+
if (next !== undefined)
|
|
32
|
+
restoreInput(next);
|
|
43
33
|
}
|
|
44
34
|
});
|
|
35
|
+
const restoreInput = (text) => {
|
|
36
|
+
const lines = text.split('\n');
|
|
37
|
+
setAccumulatedLines(lines.slice(0, -1));
|
|
38
|
+
setValue(lines.at(-1) ?? '');
|
|
39
|
+
};
|
|
45
40
|
const handleSubmit = (text) => {
|
|
46
41
|
if (text.endsWith('\\')) {
|
|
47
42
|
const newLine = text.slice(0, -1);
|
|
@@ -52,10 +47,8 @@ export function InputArea({ onSubmit, disabled, focus = true, placeholder }) {
|
|
|
52
47
|
const finalLines = [...accumulatedLines, text];
|
|
53
48
|
const fullText = finalLines.join('\n');
|
|
54
49
|
if (fullText.trim()) {
|
|
55
|
-
|
|
56
|
-
setHistory(newHistory);
|
|
50
|
+
onRememberInput(fullText);
|
|
57
51
|
}
|
|
58
|
-
setHistoryIndex(-1);
|
|
59
52
|
setAccumulatedLines([]);
|
|
60
53
|
setValue('');
|
|
61
54
|
onSubmit(fullText);
|
|
@@ -5,6 +5,7 @@ interface Props {
|
|
|
5
5
|
showThinking?: boolean | undefined;
|
|
6
6
|
verbosity?: Verbosity | undefined;
|
|
7
7
|
isRunning?: boolean | undefined;
|
|
8
|
+
onSelectionChange?: ((text: string) => void) | undefined;
|
|
8
9
|
}
|
|
9
|
-
export declare function MessageList({ mouseEnabled, messages, showThinking, verbosity, isRunning }: Props): import("react").JSX.Element;
|
|
10
|
+
export declare function MessageList({ mouseEnabled, messages, showThinking, verbosity, isRunning, onSelectionChange }: Props): import("react").JSX.Element;
|
|
10
11
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { MessageViewport } from './MessageViewport.js';
|
|
3
|
-
export function MessageList({ mouseEnabled = true, messages, showThinking, verbosity = 'normal', isRunning = false }) {
|
|
4
|
-
return _jsx(MessageViewport, { mouseEnabled: mouseEnabled, messages: messages, ...(showThinking === undefined ? {} : { showThinking }), verbosity: verbosity, isRunning: isRunning });
|
|
3
|
+
export function MessageList({ mouseEnabled = true, messages, showThinking, verbosity = 'normal', isRunning = false, onSelectionChange }) {
|
|
4
|
+
return _jsx(MessageViewport, { mouseEnabled: mouseEnabled, messages: messages, ...(showThinking === undefined ? {} : { showThinking }), verbosity: verbosity, isRunning: isRunning, ...(onSelectionChange === undefined ? {} : { onSelectionChange }) });
|
|
5
5
|
}
|
|
@@ -5,6 +5,7 @@ interface Props {
|
|
|
5
5
|
showThinking?: boolean;
|
|
6
6
|
verbosity?: Verbosity;
|
|
7
7
|
isRunning?: boolean;
|
|
8
|
+
onSelectionChange?: (text: string) => void;
|
|
8
9
|
}
|
|
9
|
-
export declare function MessageViewport({ messages, verbosity, mouseEnabled }: Props): import("react").JSX.Element;
|
|
10
|
+
export declare function MessageViewport({ messages, verbosity, mouseEnabled, onSelectionChange }: Props): import("react").JSX.Element;
|
|
10
11
|
export {};
|
|
@@ -4,7 +4,9 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
|
4
4
|
import { mouseEvent, enableMouse, disableMouse } from '../utils/mouse.js';
|
|
5
5
|
import { theme } from '../theme.js';
|
|
6
6
|
import { transcriptLines, scrollAction } from '../utils/transcript.js';
|
|
7
|
-
|
|
7
|
+
import { selectedLine, selectedText } from '../utils/selection.js';
|
|
8
|
+
import { copyToClipboard } from '../utils/clipboard.js';
|
|
9
|
+
export function MessageViewport({ messages, verbosity = 'normal', mouseEnabled = true, onSelectionChange }) {
|
|
8
10
|
const { stdout } = useStdout();
|
|
9
11
|
useEffect(() => {
|
|
10
12
|
if (!mouseEnabled || !stdout.isTTY)
|
|
@@ -15,6 +17,10 @@ export function MessageViewport({ messages, verbosity = 'normal', mouseEnabled =
|
|
|
15
17
|
const box = useRef(null);
|
|
16
18
|
const [size, setSize] = useState({ width: stdout.columns || 80, height: 12 });
|
|
17
19
|
const [start, setStart] = useState(null);
|
|
20
|
+
const [selection, setSelection] = useState();
|
|
21
|
+
const selectionRef = useRef(undefined);
|
|
22
|
+
const selecting = useRef(false);
|
|
23
|
+
const selectionAnchor = useRef(undefined);
|
|
18
24
|
useLayoutEffect(() => {
|
|
19
25
|
if (!box.current)
|
|
20
26
|
return;
|
|
@@ -26,14 +32,68 @@ export function MessageViewport({ messages, verbosity = 'normal', mouseEnabled =
|
|
|
26
32
|
const page = Math.max(1, size.height - 1);
|
|
27
33
|
const bottom = Math.max(0, lines.length - page);
|
|
28
34
|
const first = start === null ? bottom : Math.min(start, bottom);
|
|
35
|
+
const pointAt = (x, y) => {
|
|
36
|
+
if (!box.current)
|
|
37
|
+
return undefined;
|
|
38
|
+
const layout = measureElement(box.current);
|
|
39
|
+
const localY = y - layout.y;
|
|
40
|
+
if (localY < 0 || localY >= page)
|
|
41
|
+
return undefined;
|
|
42
|
+
return { line: Math.max(0, Math.min(lines.length - 1, first + localY)), column: Math.max(0, x - layout.x) };
|
|
43
|
+
};
|
|
44
|
+
const publishSelection = (next) => {
|
|
45
|
+
selectionRef.current = next;
|
|
46
|
+
setSelection(next);
|
|
47
|
+
onSelectionChange?.(next ? selectedText(lines, next) : '');
|
|
48
|
+
};
|
|
29
49
|
useInput((input, key) => {
|
|
30
50
|
const mouse = mouseEvent(input);
|
|
31
51
|
if (mouse) {
|
|
32
|
-
if (mouseEnabled && mouse.wheel)
|
|
52
|
+
if (mouseEnabled && mouse.phase === 'wheel')
|
|
33
53
|
setStart((previous) => {
|
|
34
54
|
const next = Math.max(0, Math.min(bottom, (previous === null ? bottom : Math.min(previous, bottom)) + mouse.wheel));
|
|
35
55
|
return next === bottom ? null : next;
|
|
36
56
|
});
|
|
57
|
+
const isLeftButton = (mouse.button & 3) === 0;
|
|
58
|
+
if (mouseEnabled && isLeftButton && mouse.phase === 'press') {
|
|
59
|
+
const point = pointAt(mouse.x, mouse.y);
|
|
60
|
+
if (point) {
|
|
61
|
+
selecting.current = true;
|
|
62
|
+
selectionAnchor.current = point;
|
|
63
|
+
publishSelection({ start: point, end: point });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (mouseEnabled && selecting.current && mouse.phase === 'drag') {
|
|
67
|
+
const layout = box.current ? measureElement(box.current) : undefined;
|
|
68
|
+
if (layout && mouse.y < layout.y)
|
|
69
|
+
setStart((previous) => Math.max(0, (previous === null ? bottom : previous) - 1));
|
|
70
|
+
if (layout && mouse.y >= layout.y + page)
|
|
71
|
+
setStart((previous) => Math.min(bottom, (previous === null ? bottom : previous) + 1));
|
|
72
|
+
const localY = layout ? Math.max(0, Math.min(page - 1, mouse.y - layout.y)) : mouse.y;
|
|
73
|
+
const point = pointAt(mouse.x, (layout?.y ?? 0) + localY);
|
|
74
|
+
if (point && selectionAnchor.current)
|
|
75
|
+
publishSelection({ start: selectionAnchor.current, end: point });
|
|
76
|
+
}
|
|
77
|
+
else if (selecting.current && mouse.phase === 'release') {
|
|
78
|
+
selecting.current = false;
|
|
79
|
+
const completed = selectionRef.current;
|
|
80
|
+
if (selectionAnchor.current && completed && completed.start.line === completed.end.line && completed.start.column === completed.end.column) {
|
|
81
|
+
selectionAnchor.current = undefined;
|
|
82
|
+
publishSelection(undefined);
|
|
83
|
+
}
|
|
84
|
+
else if (completed) {
|
|
85
|
+
// Terminal emulators reserve Command+C before the CLI can see it.
|
|
86
|
+
// Sync the custom selection on mouse release so the standard copy
|
|
87
|
+
// shortcut always finds the selected text in the system clipboard.
|
|
88
|
+
void copyToClipboard(selectedText(lines, completed)).catch(() => { });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (key.escape && selection) {
|
|
94
|
+
selecting.current = false;
|
|
95
|
+
selectionAnchor.current = undefined;
|
|
96
|
+
publishSelection(undefined);
|
|
37
97
|
return;
|
|
38
98
|
}
|
|
39
99
|
const action = scrollAction(input, key);
|
|
@@ -46,5 +106,5 @@ export function MessageViewport({ messages, verbosity = 'normal', mouseEnabled =
|
|
|
46
106
|
if (action === 'bottom')
|
|
47
107
|
setStart(null);
|
|
48
108
|
});
|
|
49
|
-
return (_jsxs(Box, { ref: box, flexDirection: "column", flexGrow: 1, flexShrink: 1, flexBasis: 0, minHeight: 3, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", height: page, overflow: "hidden", children: _jsx(Text, { children: lines.slice(first, first + page).join('\n') }) }), _jsx(Text, { color: theme.dim, children: start === null ? (mouseEnabled ? '滚轮 / 双指滚动 · Ctrl+G 回到底部' : 'Ctrl+P 上翻 · Ctrl+N 下翻') : `历史 ${first + 1}–${Math.min(lines.length, first + page)}/${lines.length} · 滚动浏览 · Ctrl+G 回到底部` })] }));
|
|
109
|
+
return (_jsxs(Box, { ref: box, flexDirection: "column", flexGrow: 1, flexShrink: 1, flexBasis: 0, minHeight: 3, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", height: page, overflow: "hidden", children: _jsx(Text, { children: lines.slice(first, first + page).map((line, index) => selectedLine(line, first + index, selection)).join('\n') }) }), _jsx(Text, { color: theme.dim, children: selection ? '已选中 · 松开鼠标已复制 · Ctrl+C / /copy 复制 · Esc 取消选择' : start === null ? (mouseEnabled ? '滚轮 / 双指滚动 · 拖动选择后自动复制 · Ctrl+C 复制选区 · /copy 复制回复 · Ctrl+G 回到底部' : 'Ctrl+P 上翻 · Ctrl+N 下翻 · 终端可原生选择文字') : `历史 ${first + 1}–${Math.min(lines.length, first + page)}/${lines.length} · 滚动浏览 · 拖动选择后自动复制 · Ctrl+C / /copy 复制 · Ctrl+G 回到底部` })] }));
|
|
50
110
|
}
|
|
@@ -5,8 +5,7 @@ interface Props {
|
|
|
5
5
|
model: string;
|
|
6
6
|
provider: string;
|
|
7
7
|
approvalMode: 'read-only' | 'ask' | 'auto';
|
|
8
|
-
currentStep?: string | null;
|
|
9
8
|
lanes: LaneDisplay[];
|
|
10
9
|
}
|
|
11
|
-
export declare function StatusHud({ isRunning, approvalMode,
|
|
10
|
+
export declare function StatusHud({ isRunning, approvalMode, lanes }: Props): import("react").JSX.Element;
|
|
12
11
|
export {};
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { Spinner } from './Spinner.js';
|
|
4
4
|
import { theme } from '../theme.js';
|
|
5
|
-
export function StatusHud({ isRunning, approvalMode,
|
|
5
|
+
export function StatusHud({ isRunning, approvalMode, lanes }) {
|
|
6
6
|
const active = lanes.filter((lane) => !['succeeded', 'failed', 'cancelled'].includes(lane.status));
|
|
7
|
-
return _jsxs(Box, { flexShrink: 0, gap: 1, children: [isRunning && _jsx(Spinner, {}), _jsxs(Text, { color: theme.dim, wrap: "truncate-end", children: [isRunning ?
|
|
7
|
+
return _jsxs(Box, { flexShrink: 0, gap: 1, children: [isRunning && _jsx(Spinner, {}), _jsxs(Text, { color: theme.dim, wrap: "truncate-end", children: [isRunning ? '任务进行中 · 详细进度显示在对话里 · /cancel 停止' : '就绪', " \u00B7 ", approvalMode === 'auto' ? '自动审批' : approvalMode === 'read-only' ? '只读' : '操作前询问', active.length > 1 ? ` · ${active.length} 个子任务` : '', " \u00B7 /help \u5E2E\u52A9 \u00B7 /exit \u9000\u51FA"] })] });
|
|
8
8
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -69,8 +69,6 @@ export interface PulseCliConfig {
|
|
|
69
69
|
taskRouting?: Partial<Record<PulseModelTask, string[]>>;
|
|
70
70
|
capabilities?: PulseCliCapabilities;
|
|
71
71
|
approvalMode?: 'read-only' | 'ask' | 'auto';
|
|
72
|
-
/** Parallelism is opt-in and only exposes tools explicitly classified read-only. */
|
|
73
|
-
executionMode?: 'serial' | 'parallel-read';
|
|
74
72
|
maxTurns?: number;
|
|
75
73
|
/** Percent of maxContextTokens that triggers automatic history compaction. Clamped to 1–90. */
|
|
76
74
|
autoCompactPercent?: number;
|
package/dist/config.js
CHANGED
|
@@ -8,7 +8,9 @@ export declare function useConversation({ host, conversationId, }: {
|
|
|
8
8
|
messages: DisplayMessage[];
|
|
9
9
|
error: string | null;
|
|
10
10
|
addUserMessage: (text: string) => void;
|
|
11
|
-
addAssistantMessage: (msg: DisplayMessage
|
|
11
|
+
addAssistantMessage: (msg: DisplayMessage, options?: {
|
|
12
|
+
moveToEnd?: boolean;
|
|
13
|
+
}) => void;
|
|
12
14
|
clearMessages: () => void;
|
|
13
15
|
switchConversation: (id: string) => Promise<void>;
|
|
14
16
|
newConversation: () => Promise<ConversationHandle | null>;
|
|
@@ -63,10 +63,12 @@ export function useConversation({ host, conversationId, }) {
|
|
|
63
63
|
};
|
|
64
64
|
setMessages((prev) => [...prev, newMessage]);
|
|
65
65
|
}, []);
|
|
66
|
-
const addAssistantMessage = useCallback((msg) => {
|
|
66
|
+
const addAssistantMessage = useCallback((msg, options) => {
|
|
67
67
|
setMessages((prev) => {
|
|
68
68
|
const index = prev.findIndex((m) => m.id === msg.id);
|
|
69
69
|
if (index >= 0) {
|
|
70
|
+
if (options?.moveToEnd)
|
|
71
|
+
return [...prev.filter((m) => m.id !== msg.id), msg];
|
|
70
72
|
const next = [...prev];
|
|
71
73
|
next[index] = msg;
|
|
72
74
|
return next;
|
package/dist/hooks/useRun.d.ts
CHANGED
|
@@ -4,7 +4,9 @@ export declare function describeFactStatus(data: unknown): string;
|
|
|
4
4
|
export declare function useRun({ host, conversationId, addAssistantMessage, }: {
|
|
5
5
|
host: LocalHost | null;
|
|
6
6
|
conversationId: string | null;
|
|
7
|
-
addAssistantMessage: (msg: DisplayMessage
|
|
7
|
+
addAssistantMessage: (msg: DisplayMessage, options?: {
|
|
8
|
+
moveToEnd?: boolean;
|
|
9
|
+
}) => void;
|
|
8
10
|
}): {
|
|
9
11
|
isRunning: boolean;
|
|
10
12
|
currentStep: string | null;
|