@hunterzhu/pulse-cli 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.d.ts +10 -1
- package/dist/bin.js +86 -177
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +36 -0
- package/dist/commands/interactive.d.ts +2 -0
- package/dist/commands/interactive.js +27 -0
- package/dist/commands/resume.d.ts +2 -0
- package/dist/commands/resume.js +75 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +73 -0
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +66 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +30 -0
- package/dist/commands/signals.d.ts +2 -0
- package/dist/commands/signals.js +20 -0
- package/dist/components/App.d.ts +10 -0
- package/dist/components/App.js +352 -0
- package/dist/components/ApprovalPrompt.d.ts +8 -0
- package/dist/components/ApprovalPrompt.js +21 -0
- package/dist/components/AssistantMessage.d.ts +10 -0
- package/dist/components/AssistantMessage.js +10 -0
- package/dist/components/Header.d.ts +8 -0
- package/dist/components/Header.js +8 -0
- package/dist/components/HelpView.d.ts +1 -0
- package/dist/components/HelpView.js +43 -0
- package/dist/components/InputArea.d.ts +7 -0
- package/dist/components/InputArea.js +55 -0
- package/dist/components/MessageList.d.ts +8 -0
- package/dist/components/MessageList.js +12 -0
- package/dist/components/SessionList.d.ts +15 -0
- package/dist/components/SessionList.js +33 -0
- package/dist/components/Spinner.d.ts +4 -0
- package/dist/components/Spinner.js +7 -0
- package/dist/components/ThinkingBlock.d.ts +5 -0
- package/dist/components/ThinkingBlock.js +8 -0
- package/dist/components/TokenStats.d.ts +7 -0
- package/dist/components/TokenStats.js +15 -0
- package/dist/components/ToolCallCard.d.ts +8 -0
- package/dist/components/ToolCallCard.js +31 -0
- package/dist/components/UserMessage.d.ts +5 -0
- package/dist/components/UserMessage.js +7 -0
- package/dist/components/Welcome.d.ts +6 -0
- package/dist/components/Welcome.js +6 -0
- package/dist/hooks/useConversation.d.ts +15 -0
- package/dist/hooks/useConversation.js +115 -0
- package/dist/hooks/useHost.d.ts +7 -0
- package/dist/hooks/useHost.js +44 -0
- package/dist/hooks/useMultilineInput.d.ts +11 -0
- package/dist/hooks/useMultilineInput.js +58 -0
- package/dist/hooks/useRun.d.ts +16 -0
- package/dist/hooks/useRun.js +176 -0
- package/dist/hooks/useSlashCommands.d.ts +25 -0
- package/dist/hooks/useSlashCommands.js +44 -0
- package/dist/hooks/useTokenStats.d.ts +7 -0
- package/dist/hooks/useTokenStats.js +38 -0
- package/dist/theme.d.ts +27 -0
- package/dist/theme.js +27 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +1 -0
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +20 -0
- package/dist/utils/approval.d.ts +10 -0
- package/dist/utils/approval.js +50 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.js +51 -0
- package/dist/utils/highlight.d.ts +1 -0
- package/dist/utils/highlight.js +80 -0
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +138 -0
- package/package.json +13 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { theme } from '../theme.js';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
export function SessionList({ sessions, notice, onSelect, onDelete, onBack }) {
|
|
8
|
+
const [highlightedId, setHighlightedId] = useState(sessions[0]?.id ?? null);
|
|
9
|
+
const [pendingDelete, setPendingDelete] = useState(null);
|
|
10
|
+
useInput((input) => {
|
|
11
|
+
if (pendingDelete) {
|
|
12
|
+
if (input === 'd' && highlightedId === pendingDelete && onDelete) {
|
|
13
|
+
void Promise.resolve(onDelete(pendingDelete));
|
|
14
|
+
setPendingDelete(null);
|
|
15
|
+
}
|
|
16
|
+
else if (input === 'n' || input === 'q' || input === 'd') {
|
|
17
|
+
setPendingDelete(null);
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (input === 'q') {
|
|
22
|
+
onBack();
|
|
23
|
+
}
|
|
24
|
+
else if (input === 'd' && highlightedId && onDelete) {
|
|
25
|
+
setPendingDelete(highlightedId);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const items = sessions.map(s => ({
|
|
29
|
+
label: `${s.title.length > 30 ? s.title.slice(0, 30) + '...' : s.title} (${s.updatedAt}) - ${path.basename(s.cwd)}`,
|
|
30
|
+
value: s.id
|
|
31
|
+
}));
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.primary, bold: true, children: "\u4F1A\u8BDD\u5217\u8868 (\u2191\u2193 \u9009\u62E9, Enter \u7EE7\u7EED, d \u5220\u9664, q \u8FD4\u56DE)" }) }), notice && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.error, children: notice }) })), pendingDelete && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.warning, children: "\u518D\u6309 d \u786E\u8BA4\u5220\u9664\uFF0C\u6309 n \u53D6\u6D88\u3002" }) })), items.length === 0 ? (_jsx(Text, { color: theme.dim, children: "\u6682\u65E0\u5386\u53F2\u4F1A\u8BDD\u3002\u6309 q \u8FD4\u56DE\u3002" })) : (_jsx(SelectInput, { items: items, onSelect: (item) => onSelect(item.value), onHighlight: (item) => setHighlightedId(item.value) }))] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import InkSpinner from 'ink-spinner';
|
|
4
|
+
import { theme } from '../theme.js';
|
|
5
|
+
export function Spinner({ label }) {
|
|
6
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: theme.primary, children: _jsx(InkSpinner, { type: "dots" }) }), label && _jsx(Text, { color: theme.dim, children: label })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { theme } from '../theme.js';
|
|
4
|
+
export function ThinkingBlock({ content, visible }) {
|
|
5
|
+
if (!visible)
|
|
6
|
+
return null;
|
|
7
|
+
return (_jsx(Box, { borderStyle: "single", borderLeft: true, borderTop: false, borderRight: false, borderBottom: false, borderColor: theme.thinking, paddingLeft: 1, marginY: 1, children: _jsx(Text, { color: theme.thinking, dimColor: true, children: content }) }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface TokenStatsProps {
|
|
2
|
+
inputTokens: number;
|
|
3
|
+
outputTokens: number;
|
|
4
|
+
durationMs: number;
|
|
5
|
+
estimatedCost?: number | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare function TokenStats({ inputTokens, outputTokens, durationMs, estimatedCost }: TokenStatsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { theme } from '../theme.js';
|
|
4
|
+
import { formatDuration, formatTokenCount, formatCost } from '../utils/format.js';
|
|
5
|
+
export function TokenStats({ inputTokens, outputTokens, durationMs, estimatedCost }) {
|
|
6
|
+
const duration = formatDuration(durationMs);
|
|
7
|
+
const totalTokens = formatTokenCount(inputTokens + outputTokens);
|
|
8
|
+
const inT = formatTokenCount(inputTokens);
|
|
9
|
+
const outT = formatTokenCount(outputTokens);
|
|
10
|
+
let text = `⏱ ${duration} · 📊 ${totalTokens} tokens (in: ${inT} · out: ${outT})`;
|
|
11
|
+
if (estimatedCost !== undefined) {
|
|
12
|
+
text += ` · 💰 ~${formatCost(estimatedCost)}`;
|
|
13
|
+
}
|
|
14
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.stats, dimColor: true, children: text }) }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolCallDisplay } from '../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
call: ToolCallDisplay;
|
|
4
|
+
expanded?: boolean;
|
|
5
|
+
onToggle?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function ToolCallCard({ call, expanded: defaultExpanded, onToggle }: Props): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useFocus, useInput } from 'ink';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { theme } from '../theme.js';
|
|
5
|
+
export function ToolCallCard({ call, expanded: defaultExpanded = false, onToggle }) {
|
|
6
|
+
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
|
|
7
|
+
const { isFocused } = useFocus({ autoFocus: false });
|
|
8
|
+
const isExpanded = onToggle ? defaultExpanded : internalExpanded;
|
|
9
|
+
useInput((input, key) => {
|
|
10
|
+
if (isFocused && (key.return || input === ' ')) {
|
|
11
|
+
if (onToggle) {
|
|
12
|
+
onToggle();
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
setInternalExpanded(!isExpanded);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const getStatusIcon = () => {
|
|
20
|
+
switch (call.status) {
|
|
21
|
+
case 'running': return _jsx(Text, { color: theme.warning, children: "\u23F3" });
|
|
22
|
+
case 'succeeded': return _jsx(Text, { color: theme.success, children: "\u2713" });
|
|
23
|
+
case 'failed': return _jsx(Text, { color: theme.error, children: "\u2717" });
|
|
24
|
+
default: return _jsx(Text, { children: "?" });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const argsJson = call.arguments ? JSON.stringify(call.arguments, null, 2) : '{}';
|
|
28
|
+
const resultJson = call.result ? JSON.stringify(call.result) : '';
|
|
29
|
+
const resultSummary = resultJson.length > 500 ? resultJson.slice(0, 500) + '...' : resultJson;
|
|
30
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: isFocused ? theme.tool : theme.border, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "\uD83D\uDD27" }), _jsx(Text, { color: theme.tool, bold: true, children: call.name }), getStatusIcon(), call.durationMs && _jsxs(Text, { color: theme.dim, children: [call.durationMs, "ms"] })] }), isExpanded && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.dim, children: "\u53C2\u6570:" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: theme.codeLang, children: argsJson }) }), call.result !== undefined && call.result !== null ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.dim, children: "\u7ED3\u679C:" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: theme.codeLang, children: resultSummary }) })] })) : null] }))] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
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 { stripTerminalControls } from '../utils/ansi.js';
|
|
5
|
+
export function UserMessage({ text, timestamp }) {
|
|
6
|
+
return (_jsxs(Box, { marginY: 1, gap: 1, children: [_jsx(Text, { color: theme.user, children: "\u203A" }), _jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: stripTerminalControls(text) }) }), timestamp && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: theme.dim, dimColor: true, children: timestamp }) }))] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
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
|
+
export function Welcome({ cwd, model, version }) {
|
|
5
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: theme.border, paddingX: 2, paddingY: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: theme.primary, bold: true, children: "Pulse" }), _jsxs(Text, { color: theme.dim, children: ["v", version] })] }), _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.dim, children: "Workspace: " }), _jsx(Text, { children: cwd })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.dim, children: "Model: " }), _jsx(Text, { children: model })] })] }), _jsx(Text, { color: theme.dim, children: "Type /help for commands." })] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LocalHost, ConversationHandle } from '@hunterzhu/pulse-server';
|
|
2
|
+
import type { DisplayMessage } from '../types.js';
|
|
3
|
+
export declare function useConversation({ host, conversationId, }: {
|
|
4
|
+
host: LocalHost | null;
|
|
5
|
+
conversationId?: string | undefined;
|
|
6
|
+
}): {
|
|
7
|
+
conversation: ConversationHandle | null;
|
|
8
|
+
messages: DisplayMessage[];
|
|
9
|
+
error: string | null;
|
|
10
|
+
addUserMessage: (text: string) => void;
|
|
11
|
+
addAssistantMessage: (msg: DisplayMessage) => void;
|
|
12
|
+
clearMessages: () => void;
|
|
13
|
+
switchConversation: (id: string) => Promise<void>;
|
|
14
|
+
newConversation: () => Promise<void>;
|
|
15
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
export function useConversation({ host, conversationId, }) {
|
|
3
|
+
const [conversation, setConversation] = useState(null);
|
|
4
|
+
const [messages, setMessages] = useState([]);
|
|
5
|
+
const [error, setError] = useState(null);
|
|
6
|
+
const loadMessages = useCallback(async (convId) => {
|
|
7
|
+
if (!host)
|
|
8
|
+
return;
|
|
9
|
+
try {
|
|
10
|
+
const existing = await host.getConversationMessages(convId);
|
|
11
|
+
setMessages(existing.map((m) => ({
|
|
12
|
+
id: m.id,
|
|
13
|
+
role: m.role,
|
|
14
|
+
text: m.text,
|
|
15
|
+
createdAt: m.createdAt,
|
|
16
|
+
runId: m.runId,
|
|
17
|
+
})));
|
|
18
|
+
setError(null);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
22
|
+
}
|
|
23
|
+
}, [host]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!host)
|
|
26
|
+
return;
|
|
27
|
+
const currentHost = host;
|
|
28
|
+
let mounted = true;
|
|
29
|
+
async function setupConversation() {
|
|
30
|
+
try {
|
|
31
|
+
if (conversationId) {
|
|
32
|
+
const conv = await currentHost.getConversation(conversationId);
|
|
33
|
+
if (mounted && conv) {
|
|
34
|
+
setConversation(conv);
|
|
35
|
+
await loadMessages(conversationId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const conv = await currentHost.createConversation();
|
|
40
|
+
if (mounted && conv) {
|
|
41
|
+
setConversation(conv);
|
|
42
|
+
setMessages([]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (mounted)
|
|
48
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
setupConversation();
|
|
52
|
+
return () => {
|
|
53
|
+
mounted = false;
|
|
54
|
+
};
|
|
55
|
+
}, [host, conversationId, loadMessages]);
|
|
56
|
+
const addUserMessage = useCallback((text) => {
|
|
57
|
+
const newMessage = {
|
|
58
|
+
id: Date.now().toString(),
|
|
59
|
+
role: 'user',
|
|
60
|
+
text,
|
|
61
|
+
createdAt: new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
setMessages((prev) => [...prev, newMessage]);
|
|
64
|
+
}, []);
|
|
65
|
+
const addAssistantMessage = useCallback((msg) => {
|
|
66
|
+
setMessages((prev) => {
|
|
67
|
+
const index = prev.findIndex((m) => m.id === msg.id);
|
|
68
|
+
if (index >= 0) {
|
|
69
|
+
const next = [...prev];
|
|
70
|
+
next[index] = msg;
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
return [...prev, msg];
|
|
74
|
+
});
|
|
75
|
+
}, []);
|
|
76
|
+
const clearMessages = useCallback(() => setMessages([]), []);
|
|
77
|
+
const switchConversation = useCallback(async (id) => {
|
|
78
|
+
if (!host)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
const conv = await host.getConversation(id);
|
|
82
|
+
if (conv) {
|
|
83
|
+
setConversation(conv);
|
|
84
|
+
await loadMessages(id);
|
|
85
|
+
}
|
|
86
|
+
setError(null);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
90
|
+
}
|
|
91
|
+
}, [host, loadMessages]);
|
|
92
|
+
const newConversation = useCallback(async () => {
|
|
93
|
+
if (!host)
|
|
94
|
+
return;
|
|
95
|
+
try {
|
|
96
|
+
const conv = await host.createConversation();
|
|
97
|
+
setConversation(conv);
|
|
98
|
+
setMessages([]);
|
|
99
|
+
setError(null);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
103
|
+
}
|
|
104
|
+
}, [host]);
|
|
105
|
+
return {
|
|
106
|
+
conversation,
|
|
107
|
+
messages,
|
|
108
|
+
error,
|
|
109
|
+
addUserMessage,
|
|
110
|
+
addAssistantMessage,
|
|
111
|
+
clearMessages,
|
|
112
|
+
switchConversation,
|
|
113
|
+
newConversation,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { LocalHost, LocalHostOptions } from '@hunterzhu/pulse-server';
|
|
2
|
+
export declare function closePendingHosts(): Promise<void>;
|
|
3
|
+
export declare function useHost(options: LocalHostOptions): {
|
|
4
|
+
host: LocalHost | null;
|
|
5
|
+
error: string | null;
|
|
6
|
+
ready: boolean;
|
|
7
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { createLocalHost } from '@hunterzhu/pulse-server';
|
|
3
|
+
const pendingHostCloses = [];
|
|
4
|
+
export function closePendingHosts() {
|
|
5
|
+
const pending = pendingHostCloses.splice(0, pendingHostCloses.length);
|
|
6
|
+
return Promise.all(pending).then(() => undefined);
|
|
7
|
+
}
|
|
8
|
+
export function useHost(options) {
|
|
9
|
+
const [host, setHost] = useState(null);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
const [ready, setReady] = useState(false);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
let isMounted = true;
|
|
14
|
+
let localHost = null;
|
|
15
|
+
async function initHost() {
|
|
16
|
+
try {
|
|
17
|
+
// 创建本地服务器实例
|
|
18
|
+
localHost = createLocalHost(options);
|
|
19
|
+
await localHost.init();
|
|
20
|
+
if (isMounted) {
|
|
21
|
+
setHost(localHost);
|
|
22
|
+
setReady(true);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
if (isMounted) {
|
|
27
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
initHost();
|
|
32
|
+
return () => {
|
|
33
|
+
isMounted = false;
|
|
34
|
+
if (localHost) {
|
|
35
|
+
const closing = localHost;
|
|
36
|
+
localHost = null;
|
|
37
|
+
pendingHostCloses.push(closing.close().catch((error) => {
|
|
38
|
+
console.error(error);
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}, []); // 仅在组件挂载时初始化一次
|
|
43
|
+
return { host, error, ready };
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function useMultilineInput(): {
|
|
2
|
+
currentLine: string;
|
|
3
|
+
setCurrentLine: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
4
|
+
accumulatedLines: string[];
|
|
5
|
+
isContinuation: boolean;
|
|
6
|
+
handleSubmit: (line: string) => string | null;
|
|
7
|
+
history: string[];
|
|
8
|
+
historyIndex: number;
|
|
9
|
+
navigateHistory: (direction: "up" | "down") => string | undefined;
|
|
10
|
+
addToHistory: (text: string) => void;
|
|
11
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
export function useMultilineInput() {
|
|
3
|
+
const [currentLine, setCurrentLine] = useState('');
|
|
4
|
+
const [accumulatedLines, setAccumulatedLines] = useState([]);
|
|
5
|
+
const [history, setHistory] = useState([]);
|
|
6
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
7
|
+
const isContinuation = currentLine.endsWith('\\');
|
|
8
|
+
const handleSubmit = useCallback((line) => {
|
|
9
|
+
const trimmedLine = line.trimEnd();
|
|
10
|
+
// 如果以反斜杠结尾,视为多行输入的续行
|
|
11
|
+
if (trimmedLine.endsWith('\\')) {
|
|
12
|
+
const newLine = trimmedLine.slice(0, -1);
|
|
13
|
+
setAccumulatedLines(prev => [...prev, newLine]);
|
|
14
|
+
setCurrentLine('');
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
// 否则结束多行输入
|
|
18
|
+
const finalLines = [...accumulatedLines, line];
|
|
19
|
+
const fullText = finalLines.join('\n');
|
|
20
|
+
setAccumulatedLines([]);
|
|
21
|
+
setCurrentLine('');
|
|
22
|
+
return fullText;
|
|
23
|
+
}, [accumulatedLines]);
|
|
24
|
+
const addToHistory = useCallback((text) => {
|
|
25
|
+
if (!text.trim())
|
|
26
|
+
return;
|
|
27
|
+
setHistory(prev => {
|
|
28
|
+
// 保持最多 50 条历史记录
|
|
29
|
+
const next = [text, ...prev.filter(h => h !== text)].slice(0, 50);
|
|
30
|
+
return next;
|
|
31
|
+
});
|
|
32
|
+
setHistoryIndex(-1);
|
|
33
|
+
}, []);
|
|
34
|
+
const navigateHistory = useCallback((direction) => {
|
|
35
|
+
if (history.length === 0)
|
|
36
|
+
return undefined;
|
|
37
|
+
let nextIndex = historyIndex;
|
|
38
|
+
if (direction === 'up') {
|
|
39
|
+
nextIndex = Math.min(historyIndex + 1, history.length - 1);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
nextIndex = Math.max(historyIndex - 1, -1);
|
|
43
|
+
}
|
|
44
|
+
setHistoryIndex(nextIndex);
|
|
45
|
+
return nextIndex === -1 ? '' : history[nextIndex];
|
|
46
|
+
}, [history, historyIndex]);
|
|
47
|
+
return {
|
|
48
|
+
currentLine,
|
|
49
|
+
setCurrentLine,
|
|
50
|
+
accumulatedLines,
|
|
51
|
+
isContinuation,
|
|
52
|
+
handleSubmit,
|
|
53
|
+
history,
|
|
54
|
+
historyIndex,
|
|
55
|
+
navigateHistory,
|
|
56
|
+
addToHistory
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LocalHost } from '@hunterzhu/pulse-server';
|
|
2
|
+
import type { ApprovalRequest, DisplayMessage } from '../types.js';
|
|
3
|
+
export declare function useRun({ host, conversationId, addAssistantMessage, }: {
|
|
4
|
+
host: LocalHost | null;
|
|
5
|
+
conversationId: string | null;
|
|
6
|
+
addAssistantMessage: (msg: DisplayMessage) => void;
|
|
7
|
+
}): {
|
|
8
|
+
isRunning: boolean;
|
|
9
|
+
currentStep: string | null;
|
|
10
|
+
error: string | null;
|
|
11
|
+
approvalRequest: ApprovalRequest | null;
|
|
12
|
+
sendMessage: (text: string) => Promise<void>;
|
|
13
|
+
resumeActive: () => Promise<void>;
|
|
14
|
+
approveAction: (effectId: string, approved: boolean, reason?: string) => Promise<void>;
|
|
15
|
+
cancelRun: () => Promise<void>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
function toolStatus(value) {
|
|
3
|
+
if (value === 'succeeded' || value === 'failed' || value === 'running')
|
|
4
|
+
return value;
|
|
5
|
+
return 'running';
|
|
6
|
+
}
|
|
7
|
+
export function useRun({ host, conversationId, addAssistantMessage, }) {
|
|
8
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
9
|
+
const [currentStep, setCurrentStep] = useState(null);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
const [approvalRequest, setApprovalRequest] = useState(null);
|
|
12
|
+
const runRef = useRef(null);
|
|
13
|
+
const consumeRun = useCallback(async (run) => {
|
|
14
|
+
runRef.current = run;
|
|
15
|
+
const assistantMessage = {
|
|
16
|
+
id: run.id,
|
|
17
|
+
role: 'assistant',
|
|
18
|
+
text: '',
|
|
19
|
+
createdAt: new Date().toISOString(),
|
|
20
|
+
toolCalls: [],
|
|
21
|
+
runId: run.id,
|
|
22
|
+
};
|
|
23
|
+
addAssistantMessage({ ...assistantMessage, toolCalls: [] });
|
|
24
|
+
for await (const event of run.events) {
|
|
25
|
+
switch (event.type) {
|
|
26
|
+
case 'text':
|
|
27
|
+
assistantMessage.text += String(event.data ?? '');
|
|
28
|
+
addAssistantMessage({ ...assistantMessage, toolCalls: assistantMessage.toolCalls?.map((call) => ({ ...call })) });
|
|
29
|
+
break;
|
|
30
|
+
case 'observation': {
|
|
31
|
+
const obs = event.data;
|
|
32
|
+
if (obs && typeof obs === 'object' && typeof obs.tool === 'string') {
|
|
33
|
+
const id = String(obs.toolCallId ?? obs.tool);
|
|
34
|
+
const nextCall = {
|
|
35
|
+
id,
|
|
36
|
+
name: obs.tool,
|
|
37
|
+
arguments: obs.args && typeof obs.args === 'object' && !Array.isArray(obs.args)
|
|
38
|
+
? obs.args
|
|
39
|
+
: {},
|
|
40
|
+
status: toolStatus(obs.status),
|
|
41
|
+
...(obs.result === undefined ? {} : { result: obs.result }),
|
|
42
|
+
};
|
|
43
|
+
const calls = assistantMessage.toolCalls ?? [];
|
|
44
|
+
const index = calls.findIndex((call) => call.id === id);
|
|
45
|
+
assistantMessage.toolCalls = index >= 0
|
|
46
|
+
? calls.map((call, callIndex) => callIndex === index ? { ...call, ...nextCall } : call)
|
|
47
|
+
: [...calls, nextCall];
|
|
48
|
+
addAssistantMessage({ ...assistantMessage, toolCalls: assistantMessage.toolCalls.map((call) => ({ ...call })) });
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'waiting': {
|
|
53
|
+
const payload = event.data && typeof event.data === 'object' && !Array.isArray(event.data)
|
|
54
|
+
? event.data
|
|
55
|
+
: {};
|
|
56
|
+
const effectId = typeof payload.effectId === 'string' ? payload.effectId : '';
|
|
57
|
+
const input = payload.input && typeof payload.input === 'object' && !Array.isArray(payload.input)
|
|
58
|
+
? payload.input
|
|
59
|
+
: {};
|
|
60
|
+
const tools = Array.isArray(input.tools)
|
|
61
|
+
? input.tools.flatMap((tool) => {
|
|
62
|
+
if (!tool || typeof tool !== 'object' || Array.isArray(tool))
|
|
63
|
+
return [];
|
|
64
|
+
const item = tool;
|
|
65
|
+
return [{
|
|
66
|
+
name: String(item.toolName ?? item.name ?? '系统操作'),
|
|
67
|
+
...(typeof item.toolCallId === 'string' ? { toolCallId: item.toolCallId } : {}),
|
|
68
|
+
input: item.input && typeof item.input === 'object' && !Array.isArray(item.input)
|
|
69
|
+
? item.input
|
|
70
|
+
: {},
|
|
71
|
+
}];
|
|
72
|
+
})
|
|
73
|
+
: [];
|
|
74
|
+
const firstTool = tools[0];
|
|
75
|
+
setApprovalRequest({
|
|
76
|
+
effectId,
|
|
77
|
+
toolName: tools.length === 1 ? firstTool.name : `${tools.length || 1} 个系统操作`,
|
|
78
|
+
toolArgs: firstTool?.input ?? {},
|
|
79
|
+
prompt: typeof input.prompt === 'string' ? input.prompt : '是否批准执行?',
|
|
80
|
+
...(typeof input.digest === 'string' ? { digest: input.digest } : {}),
|
|
81
|
+
...(tools.length ? { tools } : {}),
|
|
82
|
+
});
|
|
83
|
+
setCurrentStep('等待用户审批...');
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case 'fact':
|
|
87
|
+
if (typeof event.data === 'string') {
|
|
88
|
+
setCurrentStep(event.data);
|
|
89
|
+
}
|
|
90
|
+
else if (event.data && typeof event.data === 'object') {
|
|
91
|
+
setCurrentStep('正在执行...');
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case 'error':
|
|
95
|
+
setError(String(event.data ?? '发生未知错误'));
|
|
96
|
+
setIsRunning(false);
|
|
97
|
+
setCurrentStep(null);
|
|
98
|
+
setApprovalRequest(null);
|
|
99
|
+
break;
|
|
100
|
+
case 'complete':
|
|
101
|
+
setIsRunning(false);
|
|
102
|
+
setCurrentStep(null);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}, [addAssistantMessage]);
|
|
107
|
+
const finishRun = useCallback(() => {
|
|
108
|
+
setIsRunning(false);
|
|
109
|
+
setCurrentStep(null);
|
|
110
|
+
setApprovalRequest(null);
|
|
111
|
+
runRef.current = null;
|
|
112
|
+
}, []);
|
|
113
|
+
const sendMessage = useCallback(async (text) => {
|
|
114
|
+
if (!host || !conversationId || runRef.current)
|
|
115
|
+
return;
|
|
116
|
+
setIsRunning(true);
|
|
117
|
+
setError(null);
|
|
118
|
+
setCurrentStep('思考中...');
|
|
119
|
+
try {
|
|
120
|
+
const run = await host.sendMessage(conversationId, { text });
|
|
121
|
+
await consumeRun(run);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
finishRun();
|
|
128
|
+
}
|
|
129
|
+
}, [host, conversationId, consumeRun, finishRun]);
|
|
130
|
+
const resumeActive = useCallback(async () => {
|
|
131
|
+
if (!host || !conversationId || runRef.current)
|
|
132
|
+
return;
|
|
133
|
+
setIsRunning(true);
|
|
134
|
+
setError(null);
|
|
135
|
+
setCurrentStep('正在恢复未完成的运行...');
|
|
136
|
+
try {
|
|
137
|
+
const run = await host.resumeRun(conversationId);
|
|
138
|
+
await consumeRun(run);
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
finishRun();
|
|
145
|
+
}
|
|
146
|
+
}, [host, conversationId, consumeRun, finishRun]);
|
|
147
|
+
useEffect(() => () => {
|
|
148
|
+
const run = runRef.current;
|
|
149
|
+
if (run)
|
|
150
|
+
void run.cancel('USER_INTERRUPT');
|
|
151
|
+
}, []);
|
|
152
|
+
const approveAction = useCallback(async (effectId, approved, reason) => {
|
|
153
|
+
if (!effectId || !runRef.current)
|
|
154
|
+
return;
|
|
155
|
+
await runRef.current.reply(effectId, { approved, ...(approved ? {} : { reason: reason || '拒绝执行' }) });
|
|
156
|
+
setApprovalRequest(null);
|
|
157
|
+
setCurrentStep('审批已提交,正在继续...');
|
|
158
|
+
}, []);
|
|
159
|
+
const cancelRun = useCallback(async () => {
|
|
160
|
+
if (runRef.current) {
|
|
161
|
+
await runRef.current.cancel('USER_CANCELLED');
|
|
162
|
+
setIsRunning(false);
|
|
163
|
+
setCurrentStep('已取消');
|
|
164
|
+
}
|
|
165
|
+
}, []);
|
|
166
|
+
return {
|
|
167
|
+
isRunning,
|
|
168
|
+
currentStep,
|
|
169
|
+
error,
|
|
170
|
+
approvalRequest,
|
|
171
|
+
sendMessage,
|
|
172
|
+
resumeActive,
|
|
173
|
+
approveAction,
|
|
174
|
+
cancelRun,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SlashCommand } from '../types.js';
|
|
2
|
+
export interface SlashCommandDependencies {
|
|
3
|
+
onHelp?: () => void | Promise<void>;
|
|
4
|
+
onStatus?: () => void | Promise<void>;
|
|
5
|
+
onTools?: () => void | Promise<void>;
|
|
6
|
+
onArtifacts?: () => void | Promise<void>;
|
|
7
|
+
onExit?: () => void | Promise<void>;
|
|
8
|
+
onQuit?: () => void | Promise<void>;
|
|
9
|
+
onNew?: () => void | Promise<void>;
|
|
10
|
+
onSessions?: () => void | Promise<void>;
|
|
11
|
+
onDelete?: (args: string) => void | Promise<void>;
|
|
12
|
+
onExport?: (args: string) => void | Promise<void>;
|
|
13
|
+
onClear?: () => void | Promise<void>;
|
|
14
|
+
onConfig?: () => void | Promise<void>;
|
|
15
|
+
onModel?: (args: string) => void | Promise<void>;
|
|
16
|
+
onCompact?: () => void | Promise<void>;
|
|
17
|
+
onVerbose?: () => void | Promise<void>;
|
|
18
|
+
onQuiet?: () => void | Promise<void>;
|
|
19
|
+
onThinking?: (args: string) => void | Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export declare function useSlashCommands(deps: SlashCommandDependencies): {
|
|
22
|
+
commands: SlashCommand[];
|
|
23
|
+
isSlashCommand: (input: string) => boolean;
|
|
24
|
+
executeCommand: (input: string) => Promise<boolean>;
|
|
25
|
+
};
|