@hunterzhu/pulse-cli 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/bin.d.ts +1 -0
  2. package/dist/bin.js +128 -30
  3. package/dist/commands/interactive.d.ts +2 -1
  4. package/dist/commands/interactive.js +21 -2
  5. package/dist/commands/resume.js +4 -0
  6. package/dist/commands/run.js +4 -0
  7. package/dist/components/App.d.ts +2 -1
  8. package/dist/components/App.js +165 -37
  9. package/dist/components/ApprovalPrompt.d.ts +4 -1
  10. package/dist/components/ApprovalPrompt.js +20 -15
  11. package/dist/components/AskPrompt.d.ts +8 -0
  12. package/dist/components/AskPrompt.js +57 -0
  13. package/dist/components/AssistantMessage.js +2 -1
  14. package/dist/components/Header.js +2 -1
  15. package/dist/components/HelpView.js +2 -1
  16. package/dist/components/InputArea.d.ts +2 -1
  17. package/dist/components/InputArea.js +13 -3
  18. package/dist/components/MessageList.d.ts +2 -1
  19. package/dist/components/MessageList.js +4 -11
  20. package/dist/components/MessageViewport.d.ts +14 -0
  21. package/dist/components/MessageViewport.js +77 -0
  22. package/dist/components/SessionList.js +43 -13
  23. package/dist/components/StatusHud.d.ts +11 -0
  24. package/dist/components/StatusHud.js +12 -0
  25. package/dist/components/ToolCallCard.js +27 -2
  26. package/dist/components/UserMessage.js +1 -1
  27. package/dist/config.d.ts +39 -7
  28. package/dist/config.js +49 -8
  29. package/dist/hooks/useConversation.d.ts +2 -1
  30. package/dist/hooks/useConversation.js +12 -5
  31. package/dist/hooks/useRun.d.ts +7 -3
  32. package/dist/hooks/useRun.js +133 -13
  33. package/dist/hooks/useSlashCommands.d.ts +1 -0
  34. package/dist/hooks/useSlashCommands.js +1 -0
  35. package/dist/types.d.ts +23 -1
  36. package/package.json +10 -5
  37. package/scripts/postinstall.mjs +11 -0
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
- --provider <name> mock, openai-compatible, or anthropic
30
- --model <name> provider model name
31
- --base-url <url> provider endpoint
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 allow local writes and shell execution
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 providerName = option(parsed.options, 'provider') ?? process.env.PULSE_PROVIDER ?? config.provider?.provider;
103
- const model = option(parsed.options, 'model') ?? process.env.PULSE_MODEL ?? config.provider?.model;
104
- const baseURL = option(parsed.options, 'base-url') ?? process.env.PULSE_BASE_URL ?? config.provider?.baseURL;
105
- const apiKeyEnv = config.provider?.apiKeyEnv ?? (providerName === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY');
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 provider = providerName
108
- ? {
109
- provider: providerName,
110
- ...(model === undefined ? {} : { defaultModel: model }),
111
- ...(baseURL === undefined ? {} : { baseURL }),
112
- ...(apiKey === undefined ? {} : { apiKey }),
113
- }
114
- : undefined;
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 approvalMode = parsed.options['read-only'] === true
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
- ...(provider === undefined ? {} : { provider }),
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 runInteractive(options: LocalHostOptions, conversationId?: string | undefined, initialTask?: string | undefined, version?: string): Promise<number>;
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 runInteractive(options, conversationId, initialTask, version = '0.1.4') {
6
- const { waitUntilExit, unmount } = render(_jsx(App, { hostOptions: options, conversationId: conversationId, initialTask: initialTask, version: version }));
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;
@@ -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
  }
@@ -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
  }
@@ -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;
@@ -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 resumedConversation = useRef(null);
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 || !conversation || isRunning)
76
+ if (!hostReady || isRunning || !initialTask || startedTask.current)
47
77
  return;
48
- if (conversation.summary.activeRunId && resumedConversation.current !== conversation.id) {
49
- resumedConversation.current = conversation.id;
50
- if (initialTask && !startedTask.current) {
51
- startedTask.current = true;
52
- addAssistantMessage({
53
- id: `resume-hold-${Date.now()}`,
54
- role: 'system',
55
- text: '此会话有未完成的运行,已改为恢复该运行。这次附带的新任务没有发送。',
56
- createdAt: new Date().toISOString(),
57
- });
58
- }
59
- void resumeActive();
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
- if (initialTask && !startedTask.current && messages.length === 0 && !conversation.summary.activeRunId) {
63
- startedTask.current = true;
88
+ startedTask.current = true;
89
+ void (async () => {
90
+ const target = conversation ?? await createConversation();
91
+ if (!target)
92
+ return;
64
93
  addUserMessage(initialTask);
65
- void sendMessage(initialTask);
66
- }
67
- }, [hostReady, conversation, initialTask, messages.length, isRunning, addUserMessage, addAssistantMessage, sendMessage, resumeActive]);
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 model = name?.trim();
210
- if (model && host)
211
- host.setModel(model);
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: `当前模型设置为: ${model || host?.getModel() || hostOptions.provider?.defaultModel || '默认'}`,
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 || !conversation) {
332
- if (conversationError) {
333
- return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["\u4F1A\u8BDD\u52A0\u8F7D\u5931\u8D25\uFF1A", conversationError] }), _jsx(Text, { dimColor: true, children: "\u6309 n \u65B0\u5EFA\u4F1A\u8BDD\uFF0C\u6309 q \u9000\u51FA\u3002" })] }));
334
- }
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.summary.cwd;
360
- const title = conversation.summary.title;
361
- const modelName = hostOptions.provider?.defaultModel || hostOptions.provider?.provider || 'default';
362
- return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(Header, { title: title, cwd: cwd, model: modelName, approvalMode: hostOptions.approvalMode ?? 'ask' }), messages.length === 0 && !isRunning && (_jsx(Welcome, { cwd: cwd, model: modelName, version: version })), messages.length > 0 && (_jsx(MessageList, { messages: messages, showThinking: showThinking, verbosity: verbosity })), isRunning && !approvalRequest && (_jsx(Box, { marginY: 1, children: _jsx(Spinner, { label: currentStep || '正在思考与执行...' }) })), runError && (_jsx(Box, { marginY: 1, children: _jsxs(Text, { color: "red", children: ["\u9519\u8BEF: ", runError] }) })), conversationError && (_jsx(Box, { marginY: 1, children: _jsxs(Text, { color: "red", children: ["\u4F1A\u8BDD\u6D88\u606F\u52A0\u8F7D\u5931\u8D25: ", conversationError] }) })), approvalRequest && (_jsx(ApprovalPrompt, { request: approvalRequest, onApprove: () => void approveAction(approvalRequest.effectId, true), onDeny: (reason) => void approveAction(approvalRequest.effectId, false, reason) })), _jsx(Box, { marginTop: 1, children: _jsx(InputArea, { onSubmit: (txt) => void handleSubmit(txt), disabled: false, placeholder: isRunning ? '运行中也可以输入;/cancel 可取消当前运行...' : '输入消息或 /help...' }) })] }));
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 {};