@hunterzhu/pulse-cli 0.1.6 → 0.1.7
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 +1 -0
- package/dist/bin.js +128 -30
- package/dist/commands/interactive.d.ts +2 -1
- package/dist/commands/interactive.js +21 -2
- package/dist/commands/resume.js +4 -0
- package/dist/commands/run.js +4 -0
- package/dist/components/App.d.ts +2 -1
- package/dist/components/App.js +165 -37
- package/dist/components/ApprovalPrompt.d.ts +4 -1
- package/dist/components/ApprovalPrompt.js +20 -15
- package/dist/components/AskPrompt.d.ts +8 -0
- package/dist/components/AskPrompt.js +57 -0
- package/dist/components/AssistantMessage.js +2 -1
- package/dist/components/Header.js +2 -1
- package/dist/components/HelpView.js +2 -1
- package/dist/components/InputArea.d.ts +2 -1
- package/dist/components/InputArea.js +13 -3
- package/dist/components/MessageList.d.ts +2 -1
- package/dist/components/MessageList.js +4 -11
- package/dist/components/MessageViewport.d.ts +14 -0
- package/dist/components/MessageViewport.js +77 -0
- package/dist/components/SessionList.js +43 -13
- package/dist/components/StatusHud.d.ts +11 -0
- package/dist/components/StatusHud.js +12 -0
- package/dist/components/ToolCallCard.js +27 -2
- package/dist/components/UserMessage.js +1 -1
- package/dist/config.d.ts +39 -7
- package/dist/config.js +35 -8
- package/dist/hooks/useConversation.d.ts +2 -1
- package/dist/hooks/useConversation.js +12 -5
- package/dist/hooks/useRun.d.ts +7 -3
- package/dist/hooks/useRun.js +133 -13
- package/dist/hooks/useSlashCommands.d.ts +1 -0
- package/dist/hooks/useSlashCommands.js +1 -0
- package/dist/types.d.ts +23 -1
- package/package.json +2 -2
package/dist/bin.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export interface Parsed {
|
|
|
9
9
|
export declare function parse(argv: string[]): Parsed;
|
|
10
10
|
export declare function option(options: Parsed['options'], key: string): string | undefined;
|
|
11
11
|
export declare function hostOptions(parsed: Parsed): Promise<LocalHostOptions>;
|
|
12
|
+
export declare function main(): Promise<number>;
|
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
3
6
|
import { ensurePulseUserConfig, expandHome, loadPulseConfig } from './config.js';
|
|
4
7
|
import { runInteractive } from './commands/interactive.js';
|
|
5
8
|
import { runOneShot } from './commands/run.js';
|
|
@@ -17,6 +20,7 @@ const help = `Pulse ${version}
|
|
|
17
20
|
|
|
18
21
|
Usage:
|
|
19
22
|
pulse [options] start an interactive conversation
|
|
23
|
+
pulse --resume enter the latest saved session and recover an unfinished run
|
|
20
24
|
pulse run <task> [options] run one task
|
|
21
25
|
pulse sessions [options] list saved conversations
|
|
22
26
|
pulse resume <conversation-id> [task] continue or recover a conversation
|
|
@@ -26,14 +30,21 @@ Options:
|
|
|
26
30
|
--cwd <path> workspace directory
|
|
27
31
|
--data-dir <path> Pulse data directory
|
|
28
32
|
--config <path> user configuration file (default home/.pulse/config.json)
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
--
|
|
33
|
+
--model <name> Pulse model display name
|
|
34
|
+
--context-tokens <n> model context window (default 32000)
|
|
35
|
+
--max-output-tokens <n> maximum generated tokens (default 4096)
|
|
36
|
+
--reasoning-effort <x> low, medium, or high
|
|
37
|
+
--max-turns <n> maximum ReAct model/tool turns (default 32)
|
|
38
|
+
--auto-compact-percent <n> compact automatically at this percent of the context window (1-90, default 90)
|
|
32
39
|
--format <text|jsonl> output format
|
|
40
|
+
--resume enter the latest saved session
|
|
33
41
|
--read-only disable write and shell tools
|
|
34
|
-
--auto-approve
|
|
42
|
+
--auto-approve approve local writes and shell execution on your behalf
|
|
43
|
+
--approval-mode <mode> read-only, ask, or auto
|
|
35
44
|
--allow-network enable public web search and fetch tools
|
|
36
45
|
--trust-workspace treat workspace .pulse/config.json as user-trusted
|
|
46
|
+
--system-prompt <text> custom system instructions
|
|
47
|
+
--system-prompt-file <path> load custom system instructions from file
|
|
37
48
|
--mock-response <text> deterministic response for local debugging
|
|
38
49
|
--live doctor: make one real provider request
|
|
39
50
|
--no-color disable terminal styling
|
|
@@ -99,38 +110,123 @@ export async function hostOptions(parsed) {
|
|
|
99
110
|
await ensurePulseUserConfig();
|
|
100
111
|
}
|
|
101
112
|
const config = (await loadPulseConfig(requestedCwd ?? process.cwd(), explicitConfig, parsed.options['trust-workspace'] === true)).value;
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
const requestedModel = option(parsed.options, 'model') ?? process.env.PULSE_MODEL ?? config.activeModel;
|
|
114
|
+
const configuredModelEntry = requestedModel === undefined
|
|
115
|
+
? undefined
|
|
116
|
+
: Object.entries(config.models ?? {}).find(([name, item]) => name === requestedModel || item.displayName === requestedModel);
|
|
117
|
+
if (!configuredModelEntry)
|
|
118
|
+
throw new Error(`UNKNOWN_MODEL_DISPLAY_NAME:${requestedModel ?? '(missing)'}`);
|
|
119
|
+
const [, modelSelection] = configuredModelEntry;
|
|
120
|
+
const activeModelName = modelSelection.displayName;
|
|
121
|
+
const providerName = modelSelection.provider;
|
|
122
|
+
const profile = config.providers?.[providerName];
|
|
123
|
+
if (!profile)
|
|
124
|
+
throw new Error(`MODEL_PROVIDER_NOT_FOUND:${providerName}`);
|
|
125
|
+
const model = modelSelection.modelCode;
|
|
126
|
+
const baseURL = profile.baseURL;
|
|
127
|
+
const contextTokens = option(parsed.options, 'context-tokens') ?? process.env.PULSE_CONTEXT_TOKENS;
|
|
128
|
+
const maxOutputTokens = option(parsed.options, 'max-output-tokens') ?? process.env.PULSE_MAX_OUTPUT_TOKENS;
|
|
129
|
+
const reasoningEffort = option(parsed.options, 'reasoning-effort') ?? process.env.PULSE_REASONING_EFFORT;
|
|
130
|
+
const maxTurns = option(parsed.options, 'max-turns') ?? process.env.PULSE_MAX_TURNS;
|
|
131
|
+
const autoCompactPercent = option(parsed.options, 'auto-compact-percent') ?? process.env.PULSE_AUTO_COMPACT_PERCENT;
|
|
132
|
+
const apiKeyEnv = profile.apiKeyEnv ?? (profile.provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY');
|
|
106
133
|
const apiKey = process.env[apiKeyEnv];
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
134
|
+
const parsePositiveInteger = (value) => {
|
|
135
|
+
if (value === undefined)
|
|
136
|
+
return undefined;
|
|
137
|
+
const parsed = Number(value);
|
|
138
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
139
|
+
};
|
|
140
|
+
const configuredContextTokens = parsePositiveInteger(contextTokens) ?? modelSelection.maxContextTokens ?? profile.maxContextTokens;
|
|
141
|
+
const configuredMaxOutputTokens = parsePositiveInteger(maxOutputTokens) ?? modelSelection.maxOutputTokens ?? profile.maxOutputTokens;
|
|
142
|
+
const configuredReasoningEffort = reasoningEffort === 'low' || reasoningEffort === 'medium' || reasoningEffort === 'high' ? reasoningEffort : modelSelection.reasoningEffort ?? profile.reasoningEffort;
|
|
143
|
+
const configuredToolChoice = profile.toolChoice;
|
|
144
|
+
const configuredMaxTurns = parsePositiveInteger(maxTurns) ?? config.maxTurns;
|
|
145
|
+
const configuredAutoCompactPercent = parsePositiveInteger(autoCompactPercent) ?? config.autoCompactPercent;
|
|
146
|
+
const provider = {
|
|
147
|
+
provider: profile.provider,
|
|
148
|
+
defaultModel: model,
|
|
149
|
+
...(baseURL === undefined ? {} : { baseURL }),
|
|
150
|
+
...(apiKey === undefined ? {} : { apiKey }),
|
|
151
|
+
...(configuredContextTokens === undefined ? {} : { maxContextTokens: configuredContextTokens }),
|
|
152
|
+
...(configuredMaxOutputTokens === undefined ? {} : { maxOutputTokens: configuredMaxOutputTokens }),
|
|
153
|
+
...(configuredReasoningEffort === undefined ? {} : { reasoningEffort: configuredReasoningEffort }),
|
|
154
|
+
...(configuredToolChoice === undefined ? {} : { toolChoice: configuredToolChoice }),
|
|
155
|
+
};
|
|
156
|
+
const providerProfiles = Object.fromEntries(Object.entries(config.providers ?? {}).flatMap(([name, item]) => {
|
|
157
|
+
if (!item.provider)
|
|
158
|
+
throw new Error(`PROVIDER_PROTOCOL_REQUIRED:${name}`);
|
|
159
|
+
const adapterProvider = item.provider;
|
|
160
|
+
const itemKeyEnv = item.apiKeyEnv ?? (adapterProvider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY');
|
|
161
|
+
const itemKey = process.env[itemKeyEnv];
|
|
162
|
+
return [[name, {
|
|
163
|
+
provider: adapterProvider,
|
|
164
|
+
...(item.baseURL === undefined ? {} : { baseURL: item.baseURL }),
|
|
165
|
+
...(itemKey === undefined ? {} : { apiKey: itemKey }),
|
|
166
|
+
...(item.maxContextTokens === undefined ? {} : { maxContextTokens: item.maxContextTokens }),
|
|
167
|
+
...(item.maxOutputTokens === undefined ? {} : { maxOutputTokens: item.maxOutputTokens }),
|
|
168
|
+
...(item.reasoningEffort === undefined ? {} : { reasoningEffort: item.reasoningEffort }),
|
|
169
|
+
...(item.toolChoice === undefined ? {} : { toolChoice: item.toolChoice }),
|
|
170
|
+
}]];
|
|
171
|
+
}));
|
|
172
|
+
const modelDisplayNames = new Set();
|
|
173
|
+
const providerModels = Object.fromEntries(Object.entries(config.models ?? {}).flatMap(([name, item]) => {
|
|
174
|
+
if (!config.providers?.[item.provider])
|
|
175
|
+
throw new Error(`MODEL_PROVIDER_NOT_FOUND:${item.provider}`);
|
|
176
|
+
const displayName = item.displayName.trim();
|
|
177
|
+
if (!displayName)
|
|
178
|
+
throw new Error(`MODEL_DISPLAY_NAME_REQUIRED:${name}`);
|
|
179
|
+
if (modelDisplayNames.has(displayName))
|
|
180
|
+
throw new Error(`DUPLICATE_MODEL_DISPLAY_NAME:${displayName}`);
|
|
181
|
+
modelDisplayNames.add(displayName);
|
|
182
|
+
return [[displayName, { provider: item.provider, model: item.modelCode }]];
|
|
183
|
+
}));
|
|
115
184
|
const cwd = requestedCwd ?? expandHome(config.cwd);
|
|
116
185
|
const dataDir = expandHome(option(parsed.options, 'data-dir') ?? process.env.PULSE_DATA_DIR ?? config.dataDir);
|
|
117
186
|
const mockResponse = option(parsed.options, 'mock-response');
|
|
118
|
-
const
|
|
187
|
+
const requestedApprovalMode = option(parsed.options, 'approval-mode') ?? process.env.PULSE_APPROVAL_MODE;
|
|
188
|
+
const approvalMode = requestedApprovalMode === 'read-only' || parsed.options['read-only'] === true
|
|
119
189
|
? 'read-only'
|
|
120
|
-
: parsed.options['auto-approve'] === true || process.env.PULSE_AUTO_APPROVE === '1'
|
|
190
|
+
: requestedApprovalMode === 'auto' || parsed.options['auto-approve'] === true || process.env.PULSE_AUTO_APPROVE === '1'
|
|
121
191
|
? 'auto'
|
|
122
|
-
: config.approvalMode;
|
|
192
|
+
: requestedApprovalMode === 'ask' ? 'ask' : config.approvalMode;
|
|
123
193
|
const allowNetwork = parsed.options['allow-network'] === true || process.env.PULSE_ALLOW_NETWORK === '1' ? true : config.allowNetwork;
|
|
194
|
+
const rawSystemPrompt = option(parsed.options, 'system-prompt') ?? process.env.PULSE_SYSTEM_PROMPT ?? config.systemPrompt;
|
|
195
|
+
const rawSystemPromptFile = option(parsed.options, 'system-prompt-file') ?? process.env.PULSE_SYSTEM_PROMPT_FILE ?? config.systemPromptFile;
|
|
196
|
+
let systemPrompt = rawSystemPrompt;
|
|
197
|
+
if (rawSystemPromptFile) {
|
|
198
|
+
const expanded = expandHome(rawSystemPromptFile);
|
|
199
|
+
const filePath = expanded ? resolve(requestedCwd ?? process.cwd(), expanded) : undefined;
|
|
200
|
+
if (filePath) {
|
|
201
|
+
try {
|
|
202
|
+
const fileContent = await readFile(filePath, 'utf8');
|
|
203
|
+
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${fileContent.trim()}` : fileContent.trim();
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error.code === 'ENOENT') {
|
|
207
|
+
throw new Error(`SYSTEM_PROMPT_FILE_NOT_FOUND: ${filePath}`);
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
124
213
|
return {
|
|
125
214
|
...(cwd === undefined ? {} : { cwd }),
|
|
126
215
|
...(dataDir === undefined ? {} : { dataDir }),
|
|
127
|
-
...(
|
|
216
|
+
...(systemPrompt && systemPrompt.trim().length > 0 ? { systemPrompt: systemPrompt.trim() } : {}),
|
|
217
|
+
provider,
|
|
218
|
+
providerProfiles,
|
|
219
|
+
providerModels,
|
|
220
|
+
activeProviderCode: providerName,
|
|
221
|
+
activeModel: activeModelName,
|
|
128
222
|
...(mockResponse === undefined ? {} : { mockResponse }),
|
|
129
223
|
...(approvalMode === undefined ? {} : { approvalMode }),
|
|
224
|
+
...(configuredMaxTurns === undefined ? {} : { maxTurns: configuredMaxTurns }),
|
|
225
|
+
...(configuredAutoCompactPercent === undefined ? {} : { autoCompactPercent: Math.min(90, configuredAutoCompactPercent) }),
|
|
130
226
|
...(allowNetwork === undefined ? {} : { allowNetwork }),
|
|
131
227
|
};
|
|
132
228
|
}
|
|
133
|
-
async function main() {
|
|
229
|
+
export async function main() {
|
|
134
230
|
const parsed = parse(process.argv.slice(2));
|
|
135
231
|
if (parsed.options.help || parsed.options.h) {
|
|
136
232
|
process.stdout.write(help);
|
|
@@ -164,13 +260,15 @@ async function main() {
|
|
|
164
260
|
return runResume(options, id, task || undefined, option(parsed.options, 'format') ?? 'text', version);
|
|
165
261
|
}
|
|
166
262
|
// 默认启动交互式 Ink 界面
|
|
167
|
-
return runInteractive(options, undefined, undefined, version);
|
|
263
|
+
return runInteractive(options, undefined, undefined, version, parsed.options.resume === true);
|
|
264
|
+
}
|
|
265
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
266
|
+
main()
|
|
267
|
+
.then((code) => {
|
|
268
|
+
process.exitCode = code;
|
|
269
|
+
})
|
|
270
|
+
.catch((error) => {
|
|
271
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
272
|
+
process.exitCode = 1;
|
|
273
|
+
});
|
|
168
274
|
}
|
|
169
|
-
main()
|
|
170
|
-
.then((code) => {
|
|
171
|
-
process.exitCode = code;
|
|
172
|
-
})
|
|
173
|
-
.catch((error) => {
|
|
174
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
175
|
-
process.exitCode = 1;
|
|
176
|
-
});
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { LocalHostOptions } from '@hunterzhu/pulse-server';
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function findResumeConversation(options: LocalHostOptions): Promise<string | undefined>;
|
|
3
|
+
export declare function runInteractive(options: LocalHostOptions, conversationId?: string | undefined, initialTask?: string | undefined, version?: string, resumeLatest?: boolean): Promise<number>;
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from 'ink';
|
|
3
3
|
import { App } from '../components/App.js';
|
|
4
|
+
import { createLocalHost } from '@hunterzhu/pulse-server';
|
|
4
5
|
import { closePendingHosts } from '../hooks/useHost.js';
|
|
5
|
-
export async function
|
|
6
|
-
const
|
|
6
|
+
export async function findResumeConversation(options) {
|
|
7
|
+
const host = createLocalHost(options);
|
|
8
|
+
await host.init();
|
|
9
|
+
try {
|
|
10
|
+
const sessions = await host.listConversations();
|
|
11
|
+
return sessions.find((session) => session.activeRunId)?.id ?? sessions[0]?.id;
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
await host.close();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function runInteractive(options, conversationId, initialTask, version = '0.1.4', resumeLatest = false) {
|
|
18
|
+
const selectedConversationId = resumeLatest
|
|
19
|
+
? await findResumeConversation(options)
|
|
20
|
+
: conversationId;
|
|
21
|
+
const { waitUntilExit, unmount } = render(_jsx(App, { hostOptions: options, conversationId: selectedConversationId, initialTask: initialTask, resumeOnStart: resumeLatest && selectedConversationId !== undefined, version: version }), {
|
|
22
|
+
// Keep Pulse in its own full-screen buffer so the shell's previous
|
|
23
|
+
// command history does not become part of the chat workspace.
|
|
24
|
+
alternateScreen: true,
|
|
25
|
+
});
|
|
7
26
|
let interrupted = false;
|
|
8
27
|
const onSignal = () => {
|
|
9
28
|
interrupted = true;
|
package/dist/commands/resume.js
CHANGED
|
@@ -12,6 +12,10 @@ function writeEvent(event, format) {
|
|
|
12
12
|
else if (event.type === 'waiting') {
|
|
13
13
|
process.stdout.write(`\n[需要输入] ${JSON.stringify(event.data ?? '')}\n`);
|
|
14
14
|
}
|
|
15
|
+
else if (event.type === 'notice') {
|
|
16
|
+
const data = event.data && typeof event.data === 'object' && !Array.isArray(event.data) ? event.data : {};
|
|
17
|
+
process.stderr.write(`\n${data.text ?? ''}\n`);
|
|
18
|
+
}
|
|
15
19
|
else if (event.type === 'error') {
|
|
16
20
|
process.stderr.write(`\n[错误] ${String(event.data ?? '')}\n`);
|
|
17
21
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -12,6 +12,10 @@ function writeEvent(event, format) {
|
|
|
12
12
|
else if (event.type === 'waiting') {
|
|
13
13
|
process.stdout.write(`\n[需要输入] ${JSON.stringify(event.data ?? '')}\n`);
|
|
14
14
|
}
|
|
15
|
+
else if (event.type === 'notice') {
|
|
16
|
+
const data = event.data && typeof event.data === 'object' && !Array.isArray(event.data) ? event.data : {};
|
|
17
|
+
process.stderr.write(`\n${data.text ?? ''}\n`);
|
|
18
|
+
}
|
|
15
19
|
else if (event.type === 'error') {
|
|
16
20
|
process.stderr.write(`\n[错误] ${String(event.data ?? '')}\n`);
|
|
17
21
|
}
|
package/dist/components/App.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export interface AppProps {
|
|
|
4
4
|
hostOptions: LocalHostOptions;
|
|
5
5
|
conversationId?: string | undefined;
|
|
6
6
|
initialTask?: string | undefined;
|
|
7
|
+
resumeOnStart?: boolean | undefined;
|
|
7
8
|
version: string;
|
|
8
9
|
}
|
|
9
|
-
export declare function App({ hostOptions, conversationId: initialConversationId, initialTask, version, }: AppProps): React.JSX.Element;
|
|
10
|
+
export declare function App({ hostOptions, conversationId: initialConversationId, initialTask, resumeOnStart, version, }: AppProps): React.JSX.Element;
|
|
10
11
|
export default App;
|
package/dist/components/App.js
CHANGED
|
@@ -4,8 +4,10 @@ import { Box, Text, useApp, useInput } from 'ink';
|
|
|
4
4
|
import { Header } from './Header.js';
|
|
5
5
|
import { Welcome } from './Welcome.js';
|
|
6
6
|
import { MessageList } from './MessageList.js';
|
|
7
|
+
import { StatusHud } from './StatusHud.js';
|
|
7
8
|
import { Spinner } from './Spinner.js';
|
|
8
9
|
import { ApprovalPrompt } from './ApprovalPrompt.js';
|
|
10
|
+
import { AskPrompt } from './AskPrompt.js';
|
|
9
11
|
import { InputArea } from './InputArea.js';
|
|
10
12
|
import { SessionList } from './SessionList.js';
|
|
11
13
|
import { HelpView } from './HelpView.js';
|
|
@@ -14,23 +16,51 @@ import { useConversation } from '../hooks/useConversation.js';
|
|
|
14
16
|
import { useRun } from '../hooks/useRun.js';
|
|
15
17
|
import { useTokenStats } from '../hooks/useTokenStats.js';
|
|
16
18
|
import { useSlashCommands } from '../hooks/useSlashCommands.js';
|
|
17
|
-
export function App({ hostOptions, conversationId: initialConversationId, initialTask, version, }) {
|
|
19
|
+
export function App({ hostOptions, conversationId: initialConversationId, initialTask, resumeOnStart = false, version, }) {
|
|
18
20
|
const { exit } = useApp();
|
|
19
21
|
const [mode, setMode] = useState('chat');
|
|
20
22
|
const [showThinking, setShowThinking] = useState(false);
|
|
21
23
|
const [verbosity, setVerbosity] = useState('normal');
|
|
22
24
|
const [sessionsList, setSessionsList] = useState([]);
|
|
23
25
|
const [sessionError, setSessionError] = useState(null);
|
|
26
|
+
const [approvalChoiceFocused, setApprovalChoiceFocused] = useState(false);
|
|
27
|
+
const [approvalInputMode, setApprovalInputMode] = useState(false);
|
|
24
28
|
const startedTask = useRef(false);
|
|
25
|
-
const
|
|
29
|
+
const startedResume = useRef(false);
|
|
26
30
|
const { host, error: hostError, ready: hostReady } = useHost(hostOptions);
|
|
27
|
-
const { conversation, messages, addUserMessage, addAssistantMessage, clearMessages, switchConversation, newConversation, error: conversationError, } = useConversation({ host, conversationId: initialConversationId });
|
|
28
|
-
const { isRunning, currentStep, error: runError, approvalRequest, sendMessage, resumeActive, approveAction, cancelRun, } = useRun({
|
|
31
|
+
const { conversation, messages, addUserMessage, addAssistantMessage, clearMessages, switchConversation, newConversation, createConversation, error: conversationError, } = useConversation({ host, conversationId: initialConversationId });
|
|
32
|
+
const { isRunning, currentStep, error: runError, approvalRequest, askRequest, lanes, approvalSubmitting, sendMessage, resumeActive, approveAction, replyAsk, cancelRun, } = useRun({
|
|
29
33
|
host,
|
|
30
34
|
conversationId: conversation?.id ?? null,
|
|
31
35
|
addAssistantMessage,
|
|
32
36
|
});
|
|
33
37
|
const { cumulative } = useTokenStats();
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (approvalRequest) {
|
|
40
|
+
setApprovalChoiceFocused(true);
|
|
41
|
+
setApprovalInputMode(false);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
setApprovalChoiceFocused(false);
|
|
45
|
+
setApprovalInputMode(false);
|
|
46
|
+
}
|
|
47
|
+
}, [approvalRequest?.effectId]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!hostReady || !resumeOnStart || !conversation || isRunning || startedResume.current)
|
|
50
|
+
return;
|
|
51
|
+
startedResume.current = true;
|
|
52
|
+
if (conversation.summary.activeRunId) {
|
|
53
|
+
void resumeActive(conversation.id);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
addAssistantMessage({
|
|
57
|
+
id: `resume-ready-${Date.now()}`,
|
|
58
|
+
role: 'system',
|
|
59
|
+
text: `已进入上次会话:${conversation.summary.title}`,
|
|
60
|
+
createdAt: new Date().toISOString(),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}, [hostReady, resumeOnStart, conversation, isRunning, resumeActive, addAssistantMessage]);
|
|
34
64
|
const loadSessions = useCallback(async () => {
|
|
35
65
|
if (!host)
|
|
36
66
|
return;
|
|
@@ -43,28 +73,27 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
43
73
|
}
|
|
44
74
|
}, [host]);
|
|
45
75
|
useEffect(() => {
|
|
46
|
-
if (!hostReady || !
|
|
76
|
+
if (!hostReady || isRunning || !initialTask || startedTask.current)
|
|
47
77
|
return;
|
|
48
|
-
if (conversation
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
createdAt: new Date().toISOString(),
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
void resumeActive();
|
|
78
|
+
if (conversation?.summary.activeRunId) {
|
|
79
|
+
startedTask.current = true;
|
|
80
|
+
addAssistantMessage({
|
|
81
|
+
id: `resume-hold-${Date.now()}`,
|
|
82
|
+
role: 'system',
|
|
83
|
+
text: '此会话有未完成的运行。请使用 /resume 恢复运行;这次附带的新任务没有发送。',
|
|
84
|
+
createdAt: new Date().toISOString(),
|
|
85
|
+
});
|
|
60
86
|
return;
|
|
61
87
|
}
|
|
62
|
-
|
|
63
|
-
|
|
88
|
+
startedTask.current = true;
|
|
89
|
+
void (async () => {
|
|
90
|
+
const target = conversation ?? await createConversation();
|
|
91
|
+
if (!target)
|
|
92
|
+
return;
|
|
64
93
|
addUserMessage(initialTask);
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
}, [hostReady, conversation, initialTask,
|
|
94
|
+
await sendMessage(initialTask, target.id);
|
|
95
|
+
})();
|
|
96
|
+
}, [hostReady, conversation, initialTask, isRunning, addUserMessage, addAssistantMessage, createConversation, sendMessage]);
|
|
68
97
|
const { executeCommand, isSlashCommand } = useSlashCommands({
|
|
69
98
|
onHelp: () => setMode('help'),
|
|
70
99
|
onSessions: async () => {
|
|
@@ -75,6 +104,52 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
75
104
|
onNew: async () => {
|
|
76
105
|
await newConversation();
|
|
77
106
|
},
|
|
107
|
+
onResume: async () => {
|
|
108
|
+
if (!host)
|
|
109
|
+
return;
|
|
110
|
+
if (isRunning) {
|
|
111
|
+
addAssistantMessage({
|
|
112
|
+
id: `resume-busy-${Date.now()}`,
|
|
113
|
+
role: 'system',
|
|
114
|
+
text: '当前已有运行中的任务,请先等待完成或使用 /cancel。',
|
|
115
|
+
createdAt: new Date().toISOString(),
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const sessions = await host.listConversations();
|
|
121
|
+
const target = sessions.find((item) => item.activeRunId) ?? sessions[0];
|
|
122
|
+
if (!target) {
|
|
123
|
+
addAssistantMessage({
|
|
124
|
+
id: `resume-empty-${Date.now()}`,
|
|
125
|
+
role: 'system',
|
|
126
|
+
text: '还没有可恢复的会话。发送第一条消息后会自动创建会话。',
|
|
127
|
+
createdAt: new Date().toISOString(),
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
await switchConversation(target.id);
|
|
132
|
+
if (target.activeRunId) {
|
|
133
|
+
await resumeActive(target.id);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
addAssistantMessage({
|
|
137
|
+
id: `resume-done-${Date.now()}`,
|
|
138
|
+
role: 'system',
|
|
139
|
+
text: `已恢复上一次会话:${target.title}`,
|
|
140
|
+
createdAt: new Date().toISOString(),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
addAssistantMessage({
|
|
146
|
+
id: `resume-error-${Date.now()}`,
|
|
147
|
+
role: 'system',
|
|
148
|
+
text: `恢复会话失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
149
|
+
createdAt: new Date().toISOString(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
},
|
|
78
153
|
onExit: () => exit(),
|
|
79
154
|
onQuit: () => exit(),
|
|
80
155
|
onCancel: async () => {
|
|
@@ -97,8 +172,11 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
97
172
|
cwd: hostOptions.cwd ?? process.cwd(),
|
|
98
173
|
dataDir: hostOptions.dataDir ?? '默认 ~/.pulse/data',
|
|
99
174
|
provider: hostOptions.provider?.provider ?? 'mock',
|
|
100
|
-
model: hostOptions.provider?.defaultModel ?? '默认',
|
|
175
|
+
model: host?.getModel() ?? hostOptions.provider?.defaultModel ?? '默认',
|
|
176
|
+
providerProfiles: Object.keys(hostOptions.providerProfiles ?? {}),
|
|
101
177
|
approvalMode: hostOptions.approvalMode ?? 'ask',
|
|
178
|
+
maxTurns: hostOptions.maxTurns ?? 32,
|
|
179
|
+
autoCompactPercent: hostOptions.autoCompactPercent ?? 90,
|
|
102
180
|
allowNetwork: hostOptions.allowNetwork ?? false,
|
|
103
181
|
}, null, 2),
|
|
104
182
|
createdAt: new Date().toISOString(),
|
|
@@ -206,13 +284,44 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
206
284
|
}
|
|
207
285
|
},
|
|
208
286
|
onModel: (name) => {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
host
|
|
287
|
+
const selection = name?.trim();
|
|
288
|
+
try {
|
|
289
|
+
if (selection && host) {
|
|
290
|
+
const separator = selection.includes('/') ? '/' : selection.includes(':') ? ':' : undefined;
|
|
291
|
+
if (separator) {
|
|
292
|
+
const [providerName, ...modelParts] = selection.split(separator);
|
|
293
|
+
const selectedModel = modelParts.join(separator).trim();
|
|
294
|
+
if (providerName && selectedModel)
|
|
295
|
+
host.setProvider(providerName, selectedModel);
|
|
296
|
+
}
|
|
297
|
+
else if (hostOptions.providerModels?.[selection]) {
|
|
298
|
+
host.setModel(selection);
|
|
299
|
+
}
|
|
300
|
+
else if (hostOptions.providerProfiles?.[selection]) {
|
|
301
|
+
host.setProvider(selection);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
host.setModel(selection);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
addAssistantMessage({
|
|
310
|
+
id: `model-error-${Date.now()}`,
|
|
311
|
+
role: 'system',
|
|
312
|
+
text: `模型切换失败: ${error instanceof Error ? error.message : String(error)}`,
|
|
313
|
+
createdAt: new Date().toISOString(),
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const currentProvider = host?.getProvider() || hostOptions.activeProviderCode || hostOptions.provider?.provider || '默认';
|
|
318
|
+
const currentModel = host?.getModel() || hostOptions.provider?.defaultModel || '默认';
|
|
212
319
|
addAssistantMessage({
|
|
213
320
|
id: `model-${Date.now()}`,
|
|
214
321
|
role: 'system',
|
|
215
|
-
text:
|
|
322
|
+
text: selection
|
|
323
|
+
? `当前模型设置为: ${currentProvider}/${currentModel}`
|
|
324
|
+
: `当前模型: ${currentModel}\n可用模型: ${Object.keys(hostOptions.providerModels ?? {}).join(', ') || '当前模型'}\n切换示例: /model gpt5.6-a`,
|
|
216
325
|
createdAt: new Date().toISOString(),
|
|
217
326
|
});
|
|
218
327
|
},
|
|
@@ -302,10 +411,13 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
302
411
|
}
|
|
303
412
|
}
|
|
304
413
|
else {
|
|
414
|
+
const target = conversation ?? await createConversation();
|
|
415
|
+
if (!target)
|
|
416
|
+
return;
|
|
305
417
|
addUserMessage(text);
|
|
306
|
-
await sendMessage(text);
|
|
418
|
+
await sendMessage(text, target.id);
|
|
307
419
|
}
|
|
308
|
-
}, [isSlashCommand, executeCommand, addUserMessage, sendMessage]);
|
|
420
|
+
}, [isSlashCommand, executeCommand, conversation, createConversation, addUserMessage, sendMessage]);
|
|
309
421
|
useInput((input, key) => {
|
|
310
422
|
if ((hostError || (conversationError && !conversation)) && (input === 'q' || key.escape)) {
|
|
311
423
|
exit();
|
|
@@ -319,6 +431,14 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
319
431
|
void cancelRun();
|
|
320
432
|
return;
|
|
321
433
|
}
|
|
434
|
+
if (approvalRequest && input === '\t') {
|
|
435
|
+
setApprovalInputMode((inputMode) => {
|
|
436
|
+
const nextInputMode = !inputMode;
|
|
437
|
+
setApprovalChoiceFocused(!nextInputMode);
|
|
438
|
+
return nextInputMode;
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
322
442
|
if (mode === 'help') {
|
|
323
443
|
if (input === 'q' || key.escape || key.return) {
|
|
324
444
|
setMode('chat');
|
|
@@ -328,12 +448,12 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
328
448
|
if (hostError) {
|
|
329
449
|
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Pulse \u521D\u59CB\u5316\u5931\u8D25\uFF1A", hostError] }), _jsx(Text, { dimColor: true, children: "\u8BF7\u68C0\u67E5\u5DE5\u4F5C\u76EE\u5F55\u3001\u914D\u7F6E\u6587\u4EF6\u548C\u6570\u636E\u76EE\u5F55\u6743\u9650\u540E\u91CD\u8BD5\u3002" }), _jsx(Text, { dimColor: true, children: "\u6309 q \u9000\u51FA\u3002" })] }));
|
|
330
450
|
}
|
|
331
|
-
if (!hostReady
|
|
332
|
-
if (conversationError) {
|
|
333
|
-
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["\u4F1A\u8BDD\u52A0\u8F7D\u5931\u8D25\uFF1A", conversationError] }), _jsx(Text, { dimColor: true, children: "\u6309 n \u65B0\u5EFA\u4F1A\u8BDD\uFF0C\u6309 q \u9000\u51FA\u3002" })] }));
|
|
334
|
-
}
|
|
451
|
+
if (!hostReady) {
|
|
335
452
|
return (_jsx(Box, { padding: 1, children: _jsx(Spinner, { label: "\u6B63\u5728\u521D\u59CB\u5316 Pulse \u8FD0\u884C\u65F6..." }) }));
|
|
336
453
|
}
|
|
454
|
+
if (conversationError && !conversation) {
|
|
455
|
+
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["\u4F1A\u8BDD\u52A0\u8F7D\u5931\u8D25\uFF1A", conversationError] }), _jsx(Text, { dimColor: true, children: "\u6309 n \u65B0\u5EFA\u4F1A\u8BDD\uFF0C\u6309 q \u9000\u51FA\u3002" })] }));
|
|
456
|
+
}
|
|
337
457
|
if (mode === 'sessions') {
|
|
338
458
|
return (_jsx(SessionList, { sessions: sessionsList, notice: sessionError, onSelect: (id) => {
|
|
339
459
|
void switchConversation(id);
|
|
@@ -356,9 +476,17 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
356
476
|
if (mode === 'help') {
|
|
357
477
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(HelpView, {}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u6309 Enter \u6216 q \u8FD4\u56DE\u5BF9\u8BDD\u6A21\u5F0F..." }) })] }));
|
|
358
478
|
}
|
|
359
|
-
const cwd = conversation
|
|
360
|
-
const title = conversation
|
|
361
|
-
const modelName = hostOptions.provider?.defaultModel || hostOptions.provider?.provider || 'default';
|
|
362
|
-
|
|
479
|
+
const cwd = conversation?.summary.cwd ?? hostOptions.cwd ?? process.cwd();
|
|
480
|
+
const title = conversation?.summary.title ?? 'New conversation';
|
|
481
|
+
const modelName = host?.getModel() || hostOptions.provider?.defaultModel || hostOptions.provider?.provider || 'default';
|
|
482
|
+
const providerName = host?.getProvider() || hostOptions.activeProviderCode || hostOptions.provider?.provider || 'default';
|
|
483
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [_jsx(Header, { title: title, cwd: cwd, model: modelName, approvalMode: hostOptions.approvalMode ?? 'ask' }), messages.length === 0 && !isRunning && (_jsx(Welcome, { cwd: cwd, model: modelName, version: version })), messages.length > 0 && (_jsx(MessageList, { messages: messages, showThinking: showThinking, verbosity: verbosity, isRunning: isRunning })), isRunning && !approvalRequest && !askRequest && (_jsx(Box, { marginY: 1, children: _jsx(Spinner, { label: currentStep || '正在思考与执行...' }) })), runError && (_jsx(Box, { marginY: 1, children: _jsxs(Text, { color: "red", children: ["\u9519\u8BEF: ", runError] }) })), conversationError && (_jsx(Box, { marginY: 1, children: _jsxs(Text, { color: "red", children: ["\u4F1A\u8BDD\u6D88\u606F\u52A0\u8F7D\u5931\u8D25: ", conversationError] }) })), _jsx(StatusHud, { 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
|
+
void approveAction(approvalRequest.effectId, true);
|
|
485
|
+
}, onDeny: (reason) => {
|
|
486
|
+
void approveAction(approvalRequest.effectId, false, reason);
|
|
487
|
+
}, onInput: () => {
|
|
488
|
+
setApprovalChoiceFocused(false);
|
|
489
|
+
setApprovalInputMode(true);
|
|
490
|
+
} })) : 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...' }) }))] }));
|
|
363
491
|
}
|
|
364
492
|
export default App;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ApprovalRequest } from '../types.js';
|
|
2
2
|
interface Props {
|
|
3
3
|
request: ApprovalRequest;
|
|
4
|
+
isFocused?: boolean;
|
|
5
|
+
inputMode?: boolean;
|
|
4
6
|
onApprove: () => void;
|
|
5
7
|
onDeny: (reason?: string) => void;
|
|
8
|
+
onInput: () => void;
|
|
6
9
|
}
|
|
7
|
-
export declare function ApprovalPrompt({ request, onApprove, onDeny }: Props): import("react").JSX.Element;
|
|
10
|
+
export declare function ApprovalPrompt({ request, isFocused, inputMode, onApprove, onDeny, onInput }: Props): import("react").JSX.Element;
|
|
8
11
|
export {};
|