@hunterzhu/pulse-cli 0.1.11 → 0.2.1
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 +55 -0
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +104 -4
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +29 -0
- package/dist/commands/resume.js +5 -1
- package/dist/commands/run.js +7 -1
- package/dist/commands/scheduled.d.ts +2 -0
- package/dist/commands/scheduled.js +131 -0
- package/dist/commands/service.d.ts +2 -0
- package/dist/commands/service.js +81 -0
- package/dist/commands/template.d.ts +2 -0
- package/dist/commands/template.js +31 -0
- package/dist/components/App.js +8 -2
- package/dist/components/AskPrompt.js +1 -1
- package/dist/components/Header.d.ts +1 -1
- package/dist/components/Header.js +2 -3
- package/dist/components/HelpView.js +2 -1
- package/dist/components/InputArea.js +3 -1
- package/dist/components/MessageList.d.ts +2 -1
- package/dist/components/MessageList.js +2 -2
- package/dist/components/MessageViewport.d.ts +2 -6
- package/dist/components/MessageViewport.js +42 -69
- package/dist/components/StatusHud.d.ts +2 -1
- package/dist/components/StatusHud.js +4 -8
- package/dist/components/TextInput.d.ts +9 -0
- package/dist/components/TextInput.js +46 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +4 -0
- package/dist/hooks/useRun.js +33 -1
- package/dist/hooks/useSlashCommands.d.ts +1 -0
- package/dist/hooks/useSlashCommands.js +1 -0
- package/dist/utils/markdown.js +1 -1
- package/dist/utils/mouse.d.ts +6 -0
- package/dist/utils/mouse.js +11 -0
- package/dist/utils/transcript.d.ts +11 -0
- package/dist/utils/transcript.js +37 -0
- package/package.json +18 -6
package/dist/components/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
3
|
-
import { Box, Text, useApp, useInput } from 'ink';
|
|
3
|
+
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
4
4
|
import { Header } from './Header.js';
|
|
5
5
|
import { Welcome } from './Welcome.js';
|
|
6
6
|
import { MessageList } from './MessageList.js';
|
|
@@ -18,7 +18,11 @@ import { useTokenStats } from '../hooks/useTokenStats.js';
|
|
|
18
18
|
import { useSlashCommands } from '../hooks/useSlashCommands.js';
|
|
19
19
|
export function App({ hostOptions, conversationId: initialConversationId, initialTask, resumeOnStart = false, version, }) {
|
|
20
20
|
const { exit } = useApp();
|
|
21
|
+
const { stdout } = useStdout();
|
|
22
|
+
const [terminalRows, setTerminalRows] = useState(stdout.rows || 24);
|
|
23
|
+
useEffect(() => { const resize = () => setTerminalRows(stdout.rows || 24); stdout.on('resize', resize); return () => { stdout.off('resize', resize); }; }, [stdout]);
|
|
21
24
|
const [mode, setMode] = useState('chat');
|
|
25
|
+
const [mouseEnabled, setMouseEnabled] = useState(true);
|
|
22
26
|
const [showThinking, setShowThinking] = useState(false);
|
|
23
27
|
const [verbosity, setVerbosity] = useState('normal');
|
|
24
28
|
const [sessionsList, setSessionsList] = useState([]);
|
|
@@ -95,6 +99,7 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
95
99
|
})();
|
|
96
100
|
}, [hostReady, conversation, initialTask, isRunning, addUserMessage, addAssistantMessage, createConversation, sendMessage]);
|
|
97
101
|
const { executeCommand, isSlashCommand } = useSlashCommands({
|
|
102
|
+
onMouse: (arg) => setMouseEnabled((current) => arg === 'off' ? false : arg === 'on' ? true : !current),
|
|
98
103
|
onHelp: () => setMode('help'),
|
|
99
104
|
onSessions: async () => {
|
|
100
105
|
await loadSessions();
|
|
@@ -178,6 +183,7 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
178
183
|
maxTurns: hostOptions.maxTurns ?? 32,
|
|
179
184
|
autoCompactPercent: hostOptions.autoCompactPercent ?? 90,
|
|
180
185
|
allowNetwork: hostOptions.allowNetwork ?? false,
|
|
186
|
+
networkHosts: hostOptions.networkHosts ?? [],
|
|
181
187
|
}, null, 2),
|
|
182
188
|
createdAt: new Date().toISOString(),
|
|
183
189
|
});
|
|
@@ -480,7 +486,7 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
480
486
|
const title = conversation?.summary.title ?? 'New conversation';
|
|
481
487
|
const modelName = host?.getModel() || hostOptions.provider?.defaultModel || hostOptions.provider?.provider || 'default';
|
|
482
488
|
const providerName = host?.getProvider() || hostOptions.activeProviderCode || hostOptions.provider?.provider || 'default';
|
|
483
|
-
return (_jsxs(Box, { flexDirection: "column", width: "100%", height:
|
|
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', currentStep: currentStep, lanes: lanes }), approvalRequest ? (_jsx(ApprovalPrompt, { request: approvalRequest, inputMode: approvalInputMode, isFocused: approvalChoiceFocused && !approvalSubmitting, onApprove: () => {
|
|
484
490
|
void approveAction(approvalRequest.effectId, true);
|
|
485
491
|
}, onDeny: (reason) => {
|
|
486
492
|
void approveAction(approvalRequest.effectId, false, reason);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
|
-
import TextInput from '
|
|
3
|
+
import TextInput from './TextInput.js';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { theme } from '../theme.js';
|
|
6
6
|
export function AskPrompt({ request, disabled = false, onReply }) {
|
|
@@ -4,5 +4,5 @@ interface Props {
|
|
|
4
4
|
model: string;
|
|
5
5
|
approvalMode?: 'read-only' | 'ask' | 'auto' | undefined;
|
|
6
6
|
}
|
|
7
|
-
export declare function Header({ title, cwd, model
|
|
7
|
+
export declare function Header({ title, cwd, model }: Props): import("react").JSX.Element;
|
|
8
8
|
export {};
|
|
@@ -2,8 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { theme } from '../theme.js';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
export function Header({ title, cwd, model
|
|
5
|
+
export function Header({ title, cwd, model }) {
|
|
6
6
|
const cwdBasename = path.basename(cwd);
|
|
7
|
-
|
|
8
|
-
return (_jsxs(Box, { borderBottom: true, borderStyle: "single", borderColor: theme.border, borderTop: false, borderLeft: false, borderRight: false, width: "100%", justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: theme.primary, bold: true, children: " PULSE " }), _jsx(Text, { color: "white", bold: true, children: title })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: theme.dim, children: cwdBasename }), _jsx(Text, { color: theme.dim, children: "\u00B7" }), _jsx(Text, { color: theme.dim, children: model }), _jsx(Text, { color: approvalMode === 'auto' ? theme.warning : theme.dim, children: approvalLabel })] })] }));
|
|
7
|
+
return (_jsxs(Box, { borderBottom: true, borderStyle: "single", borderColor: theme.border, borderTop: false, borderLeft: false, borderRight: false, width: "100%", flexShrink: 0, justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Text, { color: theme.primary, bold: true, children: " PULSE " }), _jsx(Text, { color: "white", wrap: "truncate-end", children: title.length > 36 ? `${title.slice(0, 36)}…` : title })] }), _jsxs(Box, { gap: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Text, { color: theme.dim, wrap: "truncate-end", children: cwdBasename }), _jsx(Text, { color: theme.dim, children: "\u00B7" }), _jsx(Text, { color: theme.dim, wrap: "truncate-end", children: model })] })] }));
|
|
9
8
|
}
|
|
@@ -5,6 +5,7 @@ const COMMANDS = [
|
|
|
5
5
|
{
|
|
6
6
|
category: 'General',
|
|
7
7
|
commands: [
|
|
8
|
+
{ name: '/mouse on|off', desc: '开关滚轮/触控板滚动;关闭后可原生选择文本' },
|
|
8
9
|
{ name: '/help', desc: '显示帮助信息' },
|
|
9
10
|
{ name: '/status', desc: '查看会话状态' },
|
|
10
11
|
{ name: '/tools', desc: '列出可用工具' },
|
|
@@ -41,5 +42,5 @@ const COMMANDS = [
|
|
|
41
42
|
},
|
|
42
43
|
];
|
|
43
44
|
export function HelpView() {
|
|
44
|
-
return (
|
|
45
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "\u6EDA\u8F6E / \u89E6\u63A7\u677F\u53CC\u6307\u6EDA\u52A8 \u00B7 Ctrl+P / Ctrl+N \u7FFB\u9875 \u00B7 Ctrl+G \u56DE\u5230\u5E95\u90E8\uFF08\u4E5F\u652F\u6301 Fn+\u2191 / Fn+\u2193\uFF09" }), 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)))] }));
|
|
45
46
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
|
-
import TextInput from '
|
|
3
|
+
import TextInput from './TextInput.js';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { theme } from '../theme.js';
|
|
6
6
|
export function InputArea({ onSubmit, disabled, focus = true, placeholder }) {
|
|
@@ -11,6 +11,8 @@ export function InputArea({ onSubmit, disabled, focus = true, placeholder }) {
|
|
|
11
11
|
useInput((input, key) => {
|
|
12
12
|
if (disabled || !focus)
|
|
13
13
|
return;
|
|
14
|
+
if (key.ctrl || key.meta || key.pageUp || key.pageDown)
|
|
15
|
+
return;
|
|
14
16
|
if (key.return) {
|
|
15
17
|
if (key.shift) {
|
|
16
18
|
setAccumulatedLines((current) => [...current, value]);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { DisplayMessage, Verbosity } from '../types.js';
|
|
2
2
|
interface Props {
|
|
3
|
+
mouseEnabled?: boolean;
|
|
3
4
|
messages: DisplayMessage[];
|
|
4
5
|
showThinking?: boolean | undefined;
|
|
5
6
|
verbosity?: Verbosity | undefined;
|
|
6
7
|
isRunning?: boolean | undefined;
|
|
7
8
|
}
|
|
8
|
-
export declare function MessageList({ messages, showThinking, verbosity, isRunning }: Props): import("react").JSX.Element;
|
|
9
|
+
export declare function MessageList({ mouseEnabled, messages, showThinking, verbosity, isRunning }: Props): import("react").JSX.Element;
|
|
9
10
|
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({ messages, showThinking, verbosity = 'normal', isRunning = false }) {
|
|
4
|
-
return _jsx(MessageViewport, { messages: messages, ...(showThinking === undefined ? {} : { showThinking }), verbosity: verbosity, isRunning: isRunning });
|
|
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 });
|
|
5
5
|
}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import type { DisplayMessage, Verbosity } from '../types.js';
|
|
2
2
|
interface Props {
|
|
3
|
+
mouseEnabled?: boolean;
|
|
3
4
|
messages: DisplayMessage[];
|
|
4
5
|
showThinking?: boolean;
|
|
5
6
|
verbosity?: Verbosity;
|
|
6
7
|
isRunning?: boolean;
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
-
* A bounded message window. Ink has no built-in scroll viewport, so only the
|
|
10
|
-
* visible message window is reconciled. This keeps streaming output from
|
|
11
|
-
* re-rendering the whole transcript and leaves the header/HUD/input stable.
|
|
12
|
-
*/
|
|
13
|
-
export declare function MessageViewport({ messages, showThinking, verbosity, isRunning }: Props): import("react").JSX.Element;
|
|
9
|
+
export declare function MessageViewport({ messages, verbosity, mouseEnabled }: Props): import("react").JSX.Element;
|
|
14
10
|
export {};
|
|
@@ -1,77 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text, useInput, useStdout } from 'ink';
|
|
3
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput, useStdout, measureElement } from 'ink';
|
|
3
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { mouseEvent, enableMouse, disableMouse } from '../utils/mouse.js';
|
|
4
5
|
import { theme } from '../theme.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import { stripTerminalControls } from '../utils/ansi.js';
|
|
8
|
-
const minimumViewportRows = 5;
|
|
9
|
-
function messageWeight(message, width) {
|
|
10
|
-
const contentWidth = Math.max(24, width - 8);
|
|
11
|
-
const text = stripTerminalControls(message.text || message.thinking || '');
|
|
12
|
-
const textRows = text.split('\n').reduce((total, line) => total + Math.max(1, Math.ceil(line.length / contentWidth)), 0);
|
|
13
|
-
const toolRows = message.toolCalls?.length ? message.toolCalls.length + 1 : 0;
|
|
14
|
-
return Math.max(2, textRows + toolRows + (message.role === 'system' ? 1 : 2));
|
|
15
|
-
}
|
|
16
|
-
function renderMessage(message, showThinking, verbosity, isLast) {
|
|
17
|
-
return (_jsxs(Box, { flexDirection: "column", children: [message.role === 'user' && _jsx(UserMessage, { text: message.text, timestamp: message.createdAt }), message.role === 'assistant' && (_jsx(AssistantMessage, { text: message.text, thinking: message.thinking, showThinking: showThinking, verbosity: verbosity, toolCalls: message.toolCalls, tokenStats: message.tokenStats })), message.role === 'system' && (_jsx(Box, { paddingX: 1, marginY: 1, children: _jsx(Text, { color: theme.dim, italic: true, children: stripTerminalControls(message.text) }) })), !isLast && _jsx(Box, { height: 1 })] }, message.id));
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* A bounded message window. Ink has no built-in scroll viewport, so only the
|
|
21
|
-
* visible message window is reconciled. This keeps streaming output from
|
|
22
|
-
* re-rendering the whole transcript and leaves the header/HUD/input stable.
|
|
23
|
-
*/
|
|
24
|
-
export function MessageViewport({ messages, showThinking = false, verbosity = 'normal', isRunning = false }) {
|
|
6
|
+
import { transcriptLines, scrollAction } from '../utils/transcript.js';
|
|
7
|
+
export function MessageViewport({ messages, verbosity = 'normal', mouseEnabled = true }) {
|
|
25
8
|
const { stdout } = useStdout();
|
|
26
|
-
const [rows, setRows] = useState(stdout.rows || 24);
|
|
27
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
const onResize = () => setRows(stdout.rows || 24);
|
|
30
|
-
stdout.on('resize', onResize);
|
|
31
|
-
return () => {
|
|
32
|
-
stdout.off('resize', onResize);
|
|
33
|
-
};
|
|
34
|
-
}, [stdout]);
|
|
35
|
-
const viewportRows = Math.max(minimumViewportRows, rows - (isRunning ? 9 : 7));
|
|
36
|
-
const width = stdout.columns || 100;
|
|
37
|
-
const weights = useMemo(() => messages.map((message) => messageWeight(message, width)), [messages, width]);
|
|
38
|
-
const totalRows = weights.reduce((sum, weight) => sum + weight, 0);
|
|
39
|
-
const maxScroll = Math.max(0, totalRows - viewportRows);
|
|
40
9
|
useEffect(() => {
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}, [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
10
|
+
if (!mouseEnabled || !stdout.isTTY)
|
|
11
|
+
return;
|
|
12
|
+
stdout.write(enableMouse);
|
|
13
|
+
return () => { stdout.write(disableMouse); };
|
|
14
|
+
}, [mouseEnabled, stdout]);
|
|
15
|
+
const box = useRef(null);
|
|
16
|
+
const [size, setSize] = useState({ width: stdout.columns || 80, height: 12 });
|
|
17
|
+
const [start, setStart] = useState(null);
|
|
18
|
+
useLayoutEffect(() => {
|
|
19
|
+
if (!box.current)
|
|
20
|
+
return;
|
|
21
|
+
const measured = measureElement(box.current);
|
|
22
|
+
setSize((previous) => previous.width === measured.width && previous.height === measured.height ? previous : measured);
|
|
23
|
+
});
|
|
24
|
+
useEffect(() => { const resize = () => setSize((s) => ({ ...s, width: stdout.columns || 80 })); stdout.on('resize', resize); return () => { stdout.off('resize', resize); }; }, [stdout]);
|
|
25
|
+
const lines = useMemo(() => transcriptLines(messages, size.width, verbosity), [messages, size.width, verbosity]);
|
|
26
|
+
const page = Math.max(1, size.height - 1);
|
|
27
|
+
const bottom = Math.max(0, lines.length - page);
|
|
28
|
+
const first = start === null ? bottom : Math.min(start, bottom);
|
|
29
|
+
useInput((input, key) => {
|
|
30
|
+
const mouse = mouseEvent(input);
|
|
31
|
+
if (mouse) {
|
|
32
|
+
if (mouseEnabled && mouse.wheel)
|
|
33
|
+
setStart((previous) => {
|
|
34
|
+
const next = Math.max(0, Math.min(bottom, (previous === null ? bottom : Math.min(previous, bottom)) + mouse.wheel));
|
|
35
|
+
return next === bottom ? null : next;
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
55
38
|
}
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
const action = scrollAction(input, key);
|
|
40
|
+
if (action === 'up')
|
|
41
|
+
setStart(Math.max(0, first - Math.max(1, page - 2)));
|
|
42
|
+
if (action === 'down') {
|
|
43
|
+
const next = Math.min(bottom, first + Math.max(1, page - 2));
|
|
44
|
+
setStart(next === bottom ? null : next);
|
|
58
45
|
}
|
|
46
|
+
if (action === 'bottom')
|
|
47
|
+
setStart(null);
|
|
59
48
|
});
|
|
60
|
-
|
|
61
|
-
// bottom, maxScroll means show the oldest visible window.
|
|
62
|
-
let remaining = Math.max(0, totalRows - viewportRows - scrollOffset);
|
|
63
|
-
let startIndex = 0;
|
|
64
|
-
while (startIndex < weights.length && remaining >= weights[startIndex]) {
|
|
65
|
-
remaining -= weights[startIndex];
|
|
66
|
-
startIndex++;
|
|
67
|
-
}
|
|
68
|
-
let visibleRows = 0;
|
|
69
|
-
let endIndex = startIndex;
|
|
70
|
-
while (endIndex < messages.length && visibleRows < viewportRows + 1) {
|
|
71
|
-
visibleRows += weights[endIndex];
|
|
72
|
-
endIndex++;
|
|
73
|
-
}
|
|
74
|
-
const visibleMessages = messages.slice(startIndex, endIndex);
|
|
75
|
-
const isAtBottom = scrollOffset === 0;
|
|
76
|
-
return (_jsxs(Box, { flexDirection: "column", height: viewportRows, overflow: "hidden", children: [!isAtBottom && _jsx(Text, { color: theme.dim, children: "\u2191 PageUp \u67E5\u770B\u66F4\u65E9\u6D88\u606F \u00B7 PageDown \u8FD4\u56DE\u6700\u65B0" }), visibleMessages.length === 0 ? (_jsx(Box, { flexGrow: 1 })) : (visibleMessages.map((message, index) => renderMessage(message, showThinking, verbosity, startIndex + index === messages.length - 1))), maxScroll > 0 && isAtBottom && _jsx(Text, { color: theme.dim, children: "\u2193 \u5DF2\u5728\u6700\u65B0\u6D88\u606F \u00B7 PageUp \u6EDA\u52A8\u5386\u53F2" })] }));
|
|
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 回到底部` })] }));
|
|
77
50
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LaneDisplay } from '../types.js';
|
|
2
2
|
interface Props {
|
|
3
|
+
isRunning?: boolean;
|
|
3
4
|
cwd: string;
|
|
4
5
|
model: string;
|
|
5
6
|
provider: string;
|
|
@@ -7,5 +8,5 @@ interface Props {
|
|
|
7
8
|
currentStep?: string | null;
|
|
8
9
|
lanes: LaneDisplay[];
|
|
9
10
|
}
|
|
10
|
-
export declare function StatusHud({
|
|
11
|
+
export declare function StatusHud({ isRunning, approvalMode, currentStep, lanes }: Props): import("react").JSX.Element;
|
|
11
12
|
export {};
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { Spinner } from './Spinner.js';
|
|
3
4
|
import { theme } from '../theme.js';
|
|
4
|
-
function
|
|
5
|
-
const
|
|
6
|
-
return
|
|
7
|
-
}
|
|
8
|
-
export function StatusHud({ cwd, model, provider, approvalMode, currentStep, lanes }) {
|
|
9
|
-
const activeLanes = lanes.filter((lane) => !['succeeded', 'failed', 'cancelled'].includes(lane.status));
|
|
10
|
-
const approvalLabel = approvalMode === 'auto' ? 'auto' : approvalMode === 'read-only' ? '只读' : 'ask';
|
|
11
|
-
return (_jsxs(Box, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: theme.border, flexDirection: "column", paddingTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: theme.primary, bold: true, children: "STATUS" }), _jsxs(Text, { color: theme.dim, children: ["model:", model] }), _jsxs(Text, { color: theme.dim, children: ["provider:", provider] }), _jsxs(Text, { color: approvalMode === 'auto' ? theme.warning : theme.dim, children: ["approval:", approvalLabel] }), _jsxs(Text, { color: activeLanes.length ? theme.accent : theme.dim, children: ["lanes:", activeLanes.length, "/", lanes.length] }), currentStep && _jsxs(Text, { color: theme.dim, children: ["\u00B7 ", currentStep] })] }), _jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: theme.dim, children: ["workspace:", cwd] }) }), lanes.length > 0 && (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: lanes.slice(-4).map((lane) => (_jsx(Text, { color: lane.status === 'failed' ? theme.error : lane.status === 'succeeded' ? theme.success : theme.dim, children: laneLabel(lane) }, lane.id))) }))] }));
|
|
5
|
+
export function StatusHud({ isRunning, approvalMode, currentStep, lanes }) {
|
|
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 ? currentStep || '正在执行' : '就绪', " \u00B7 ", approvalMode === 'auto' ? '自动审批' : approvalMode === 'read-only' ? '只读' : '操作前询问', active.length > 1 ? ` · ${active.length} 个子任务` : '', " \u00B7 /help \u5E2E\u52A9 \u00B7 /verbose \u8BE6\u60C5"] })] });
|
|
12
8
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
value: string;
|
|
3
|
+
onChange: (value: string) => void;
|
|
4
|
+
focus?: boolean;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Keep global control shortcuts out of editable text, including on Mac terminals. */
|
|
8
|
+
export default function TextInput({ value, onChange, focus, placeholder }: Props): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text, useInput } from 'ink';
|
|
3
|
+
import { useRef, useState } from 'react';
|
|
4
|
+
import { mouseEvent } from '../utils/mouse.js';
|
|
5
|
+
import { stripTerminalControls } from '../utils/ansi.js';
|
|
6
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
7
|
+
/** Keep global control shortcuts out of editable text, including on Mac terminals. */
|
|
8
|
+
export default function TextInput({ value, onChange, focus = true, placeholder = '' }) {
|
|
9
|
+
const [position, setPosition] = useState(null);
|
|
10
|
+
const editing = useRef({ value, position });
|
|
11
|
+
editing.current = { value, position };
|
|
12
|
+
const characters = Array.from(segmenter.segment(value), (part) => part.segment);
|
|
13
|
+
const cursor = position === null ? characters.length : Math.min(position, characters.length);
|
|
14
|
+
useInput((input, key) => {
|
|
15
|
+
const characters = Array.from(segmenter.segment(editing.current.value), (part) => part.segment);
|
|
16
|
+
const cursor = editing.current.position === null ? characters.length : Math.min(editing.current.position, characters.length);
|
|
17
|
+
const move = (next) => { editing.current.position = next; setPosition(next); };
|
|
18
|
+
if (mouseEvent(input))
|
|
19
|
+
return;
|
|
20
|
+
if (key.ctrl || key.meta || key.pageUp || key.pageDown || key.upArrow || key.downArrow || key.tab || key.escape || key.return)
|
|
21
|
+
return;
|
|
22
|
+
if (key.leftArrow || key.rightArrow || key.home || key.end) {
|
|
23
|
+
move(key.home ? 0 : key.end ? null : Math.max(0, Math.min(characters.length, cursor + (key.leftArrow ? -1 : 1))));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const next = [...characters];
|
|
27
|
+
if (key.backspace || key.delete) {
|
|
28
|
+
if (!cursor)
|
|
29
|
+
return;
|
|
30
|
+
next.splice(cursor - 1, 1);
|
|
31
|
+
move(cursor - 1 === next.length ? null : cursor - 1);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const inserted = Array.from(segmenter.segment(stripTerminalControls(input)), (part) => part.segment);
|
|
35
|
+
if (!inserted.length)
|
|
36
|
+
return;
|
|
37
|
+
next.splice(cursor, 0, ...inserted);
|
|
38
|
+
move(cursor === characters.length ? null : cursor + inserted.length);
|
|
39
|
+
}
|
|
40
|
+
editing.current.value = next.join('');
|
|
41
|
+
onChange(editing.current.value);
|
|
42
|
+
}, { isActive: focus });
|
|
43
|
+
if (!value)
|
|
44
|
+
return _jsxs(Text, { dimColor: true, children: [focus ? _jsx(Text, { inverse: true, children: placeholder.slice(0, 1) || ' ' }) : null, focus ? placeholder.slice(1) : placeholder] });
|
|
45
|
+
return _jsxs(Text, { children: [characters.slice(0, cursor).join(''), focus ? _jsx(Text, { inverse: true, children: characters[cursor] || ' ' }) : characters[cursor], characters.slice(cursor + 1).join('')] });
|
|
46
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -24,6 +24,35 @@ export interface PulseCliModel {
|
|
|
24
24
|
maxContextTokens?: number;
|
|
25
25
|
maxOutputTokens?: number;
|
|
26
26
|
reasoningEffort?: 'low' | 'medium' | 'high';
|
|
27
|
+
pricing?: {
|
|
28
|
+
currency: string;
|
|
29
|
+
inputPerMillion: number;
|
|
30
|
+
outputPerMillion: number;
|
|
31
|
+
version: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export type PulseModelTask = 'reason' | 'plan' | 'merge' | 'verify';
|
|
35
|
+
export interface PulseCliMcpServer {
|
|
36
|
+
command: string;
|
|
37
|
+
args?: string[];
|
|
38
|
+
cwd?: string;
|
|
39
|
+
/** Explicit environment overrides; secret values should be sourced through provider-specific env mechanisms where possible. */
|
|
40
|
+
env?: Record<string, string>;
|
|
41
|
+
/** Map child environment names to names of variables in Pulse's launch environment. */
|
|
42
|
+
envFrom?: Record<string, string>;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
/** Explicit trust decision for each remote tool; all unspecified tools remain external. */
|
|
45
|
+
toolPolicies?: Record<string, 'read' | 'write' | 'external'>;
|
|
46
|
+
}
|
|
47
|
+
export interface PulseCliCapabilities {
|
|
48
|
+
/** IDs must be explicitly enabled; workspace config cannot enable host extensions. */
|
|
49
|
+
enabled?: string[];
|
|
50
|
+
/** Names of host-installed skills to load as untrusted instructions. */
|
|
51
|
+
skills?: string[];
|
|
52
|
+
/** Additional absolute roots explicitly trusted by the user for installed skills. */
|
|
53
|
+
trustedSkillRoots?: string[];
|
|
54
|
+
/** Host-level MCP processes; these are executable trusted configuration. */
|
|
55
|
+
mcpServers?: Record<string, PulseCliMcpServer>;
|
|
27
56
|
}
|
|
28
57
|
export interface PulseCliConfig {
|
|
29
58
|
cwd?: string;
|
|
@@ -36,10 +65,17 @@ export interface PulseCliConfig {
|
|
|
36
65
|
models?: Record<string, PulseCliModel>;
|
|
37
66
|
/** Active Pulse model display name. */
|
|
38
67
|
activeModel?: string;
|
|
68
|
+
/** Ordered model display names per task; later entries act as fallbacks. */
|
|
69
|
+
taskRouting?: Partial<Record<PulseModelTask, string[]>>;
|
|
70
|
+
capabilities?: PulseCliCapabilities;
|
|
39
71
|
approvalMode?: 'read-only' | 'ask' | 'auto';
|
|
72
|
+
/** Parallelism is opt-in and only exposes tools explicitly classified read-only. */
|
|
73
|
+
executionMode?: 'serial' | 'parallel-read';
|
|
40
74
|
maxTurns?: number;
|
|
41
75
|
/** Percent of maxContextTokens that triggers automatic history compaction. Clamped to 1–90. */
|
|
42
76
|
autoCompactPercent?: number;
|
|
77
|
+
/** Explicit network destinations; workspace configuration cannot grant these. */
|
|
78
|
+
networkHosts?: string[];
|
|
43
79
|
allowNetwork?: boolean;
|
|
44
80
|
}
|
|
45
81
|
export declare const defaultPulseConfig: PulseCliConfig;
|
package/dist/config.js
CHANGED
|
@@ -24,6 +24,7 @@ export const defaultPulseConfig = {
|
|
|
24
24
|
},
|
|
25
25
|
activeModel: 'mock',
|
|
26
26
|
approvalMode: 'ask',
|
|
27
|
+
executionMode: 'serial',
|
|
27
28
|
maxTurns: 32,
|
|
28
29
|
autoCompactPercent: 90,
|
|
29
30
|
allowNetwork: false,
|
|
@@ -81,8 +82,11 @@ export function sanitizeWorkspaceConfig(value) {
|
|
|
81
82
|
return {
|
|
82
83
|
...(value.cwd === undefined ? {} : { cwd: value.cwd }),
|
|
83
84
|
...(value.dataDir === undefined ? {} : { dataDir: value.dataDir }),
|
|
85
|
+
// Workspace configs cannot launch processes, select host skills, or enable installed capabilities.
|
|
84
86
|
...(providers === undefined || Object.keys(providers).length === 0 ? {} : { providers }),
|
|
85
87
|
...(value.activeModel === undefined ? {} : { activeModel: value.activeModel }),
|
|
88
|
+
// Workspace configs may select a model, but routing affects provider and
|
|
89
|
+
// data-sharing choices and therefore remains user-config-only.
|
|
86
90
|
...(value.models === undefined ? {} : {
|
|
87
91
|
models: Object.fromEntries(Object.entries(value.models).flatMap(([name, model]) => {
|
|
88
92
|
if (!model.provider || !model.modelCode)
|
package/dist/hooks/useRun.js
CHANGED
|
@@ -184,7 +184,9 @@ export function useRun({ host, conversationId, addAssistantMessage, }) {
|
|
|
184
184
|
break;
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
-
|
|
187
|
+
// Runtime facts are diagnostic data, not user-facing prose.
|
|
188
|
+
if (typeof event.data === 'string' && event.data.startsWith('human.input.'))
|
|
189
|
+
setCurrentStep(describeFactStatus(event.data));
|
|
188
190
|
break;
|
|
189
191
|
case 'error':
|
|
190
192
|
setError(String(event.data ?? '发生未知错误'));
|
|
@@ -201,6 +203,36 @@ export function useRun({ host, conversationId, addAssistantMessage, }) {
|
|
|
201
203
|
setAskRequest(null);
|
|
202
204
|
if (event.data && typeof event.data === 'object' && !Array.isArray(event.data)) {
|
|
203
205
|
const completion = event.data;
|
|
206
|
+
const usage = completion.usage && typeof completion.usage === 'object' && !Array.isArray(completion.usage)
|
|
207
|
+
? completion.usage
|
|
208
|
+
: undefined;
|
|
209
|
+
if (usage) {
|
|
210
|
+
const input = typeof usage.inputTokens === 'number' ? usage.inputTokens.toLocaleString() : '未知';
|
|
211
|
+
const output = typeof usage.outputTokens === 'number' ? usage.outputTokens.toLocaleString() : '未知';
|
|
212
|
+
const costs = Array.isArray(usage.providerCosts) ? usage.providerCosts.filter((item) => Boolean(item && typeof item === 'object' && typeof item.currency === 'string' && typeof item.amount === 'number')) : [];
|
|
213
|
+
const estimate = usage.estimatedCost && typeof usage.estimatedCost === 'object' ? usage.estimatedCost : undefined;
|
|
214
|
+
const costLabel = costs.length ? costs.map((item) => `${item.amount} ${item.currency}`).join(', ') : estimate && typeof estimate.amount === 'number' ? `估算 ${estimate.amount} ${String(estimate.currency)} (价格 ${String(estimate.pricingVersion)})` : '费用未知';
|
|
215
|
+
addAssistantMessage({ id: `run-usage-${run.id}`, role: 'system', text: `用量:输入 ${input} · 输出 ${output} · ${costLabel}${usage.completeness === 'partial' ? ' · 数据不完整' : ''}`, createdAt: new Date().toISOString(), runId: run.id });
|
|
216
|
+
}
|
|
217
|
+
const taskOutcome = completion.taskOutcome && typeof completion.taskOutcome === 'object' && !Array.isArray(completion.taskOutcome)
|
|
218
|
+
? completion.taskOutcome
|
|
219
|
+
: undefined;
|
|
220
|
+
if (taskOutcome && typeof taskOutcome.status === 'string') {
|
|
221
|
+
const labels = {
|
|
222
|
+
accepted: '任务验收通过',
|
|
223
|
+
incomplete: '任务未完成',
|
|
224
|
+
unverifiable: '任务结果无法核验',
|
|
225
|
+
failed: '任务执行失败',
|
|
226
|
+
cancelled: '任务已取消',
|
|
227
|
+
};
|
|
228
|
+
addAssistantMessage({
|
|
229
|
+
id: `task-outcome-${run.id}`,
|
|
230
|
+
role: 'system',
|
|
231
|
+
text: labels[taskOutcome.status] ?? `任务验收状态:${taskOutcome.status}`,
|
|
232
|
+
createdAt: new Date().toISOString(),
|
|
233
|
+
runId: run.id,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
204
236
|
const completionError = completion.error;
|
|
205
237
|
if (completion.status === 'failed' && completionError && typeof completionError === 'object' && !Array.isArray(completionError)) {
|
|
206
238
|
const error = completionError;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo, useCallback } from 'react';
|
|
2
2
|
export function useSlashCommands(deps) {
|
|
3
3
|
const commands = useMemo(() => [
|
|
4
|
+
{ name: '/mouse', description: '滚轮和触控板滚动 [on|off]', execute: async (args) => deps.onMouse?.(args) },
|
|
4
5
|
{ name: '/help', aliases: ['/h'], description: '显示帮助信息', execute: async () => deps.onHelp?.() },
|
|
5
6
|
{ name: '/status', description: '查看状态', execute: async () => deps.onStatus?.() },
|
|
6
7
|
{ name: '/tools', description: '列出可用工具', execute: async () => deps.onTools?.() },
|
package/dist/utils/markdown.js
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** SGR mouse reporting. Ink removes the first ESC before delivering unknown keys. */
|
|
2
|
+
export declare function mouseEvent(input: string): {
|
|
3
|
+
wheel: number;
|
|
4
|
+
} | undefined;
|
|
5
|
+
export declare const enableMouse = "\u001B[?1000h\u001B[?1006h";
|
|
6
|
+
export declare const disableMouse = "\u001B[?1006l\u001B[?1000l";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** SGR mouse reporting. Ink removes the first ESC before delivering unknown keys. */
|
|
2
|
+
export function mouseEvent(input) {
|
|
3
|
+
const match = /^(?:\x1b)?\[<(\d+);\d+;\d+([Mm])$/.exec(input);
|
|
4
|
+
if (!match)
|
|
5
|
+
return undefined;
|
|
6
|
+
const button = Number(match[1]);
|
|
7
|
+
const base = button & ~28; // Shift, Alt and Ctrl modifiers do not change direction.
|
|
8
|
+
return { wheel: match[2] === 'M' && base === 64 ? -3 : match[2] === 'M' && base === 65 ? 3 : 0 };
|
|
9
|
+
}
|
|
10
|
+
export const enableMouse = '\x1b[?1000h\x1b[?1006h';
|
|
11
|
+
export const disableMouse = '\x1b[?1006l\x1b[?1000l';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DisplayMessage, Verbosity } from '../types.js';
|
|
2
|
+
/** One shared rendering and wrapping path; scroll offsets are actual terminal rows. */
|
|
3
|
+
export declare function transcriptLines(messages: DisplayMessage[], width: number, verbosity?: Verbosity): string[];
|
|
4
|
+
export declare function scrollAction(input: string, key: {
|
|
5
|
+
ctrl?: boolean;
|
|
6
|
+
pageUp?: boolean;
|
|
7
|
+
pageDown?: boolean;
|
|
8
|
+
meta?: boolean;
|
|
9
|
+
upArrow?: boolean;
|
|
10
|
+
downArrow?: boolean;
|
|
11
|
+
}): 'up' | 'down' | 'bottom' | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import wrapAnsi from 'wrap-ansi';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { stripTerminalControls } from './ansi.js';
|
|
4
|
+
import { renderMarkdownToAnsi } from './markdown.js';
|
|
5
|
+
/** One shared rendering and wrapping path; scroll offsets are actual terminal rows. */
|
|
6
|
+
export function transcriptLines(messages, width, verbosity = 'normal') {
|
|
7
|
+
const blocks = messages.map((message) => {
|
|
8
|
+
const text = stripTerminalControls(message.text);
|
|
9
|
+
if (message.role === 'user')
|
|
10
|
+
return chalk.cyan(`你 › ${text}`);
|
|
11
|
+
if (message.role === 'system')
|
|
12
|
+
return chalk.dim(text);
|
|
13
|
+
const parts = [];
|
|
14
|
+
const tools = message.toolCalls ?? [];
|
|
15
|
+
if (verbosity !== 'quiet' && tools.length) {
|
|
16
|
+
const failed = tools.filter((tool) => tool.status === 'failed' || tool.status === 'cancelled');
|
|
17
|
+
parts.push(chalk.dim(`工具 · ${tools.filter((tool) => tool.status === 'succeeded').length}/${tools.length} 已完成${failed.length ? ` · ${failed.length} 未成功` : ''}`));
|
|
18
|
+
for (const tool of verbosity === 'verbose' ? tools : failed) {
|
|
19
|
+
const detail = `${tool.name} · ${tool.status}${verbosity === 'verbose' ? `\n${JSON.stringify(tool.arguments ?? {}, null, 2)}` : ''}${tool.result === undefined ? '' : `\n${typeof tool.result === 'string' ? tool.result : JSON.stringify(tool.result, null, 2)}`}`;
|
|
20
|
+
parts.push(chalk.dim(stripTerminalControls(detail)));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (text)
|
|
24
|
+
parts.push(chalk.cyan.bold('Pulse'), renderMarkdownToAnsi(text));
|
|
25
|
+
return parts.join('\n');
|
|
26
|
+
}).filter(Boolean);
|
|
27
|
+
return wrapAnsi(blocks.join('\n\n'), Math.max(8, width), { hard: true, trim: false }).split('\n');
|
|
28
|
+
}
|
|
29
|
+
export function scrollAction(input, key) {
|
|
30
|
+
if (key.pageUp || (key.ctrl && input === 'p'))
|
|
31
|
+
return 'up';
|
|
32
|
+
if (key.pageDown || (key.ctrl && input === 'n'))
|
|
33
|
+
return 'down';
|
|
34
|
+
if (key.ctrl && input === 'g')
|
|
35
|
+
return 'bottom';
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|