@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.
Files changed (36) 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 +35 -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 +2 -2
package/dist/config.js CHANGED
@@ -2,8 +2,16 @@ import { access, chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  export const defaultPulseConfig = {
5
- provider: { provider: 'mock', model: 'mock', apiKeyEnv: 'OPENAI_API_KEY' },
5
+ providers: {
6
+ mock: { provider: 'mock', name: 'Mock' },
7
+ },
8
+ models: {
9
+ mock: { displayName: 'mock', provider: 'mock', modelCode: 'mock', maxContextTokens: 32_000, maxOutputTokens: 4_096, reasoningEffort: 'medium' },
10
+ },
11
+ activeModel: 'mock',
6
12
  approvalMode: 'ask',
13
+ maxTurns: 32,
14
+ autoCompactPercent: 90,
7
15
  allowNetwork: false,
8
16
  };
9
17
  export function defaultPulseConfigPath() {
@@ -43,16 +51,34 @@ function asConfig(value) {
43
51
  return undefined;
44
52
  return value;
45
53
  }
46
- /** Workspace files must not escalate approval, network, or provider credentials. */
54
+ /** Workspace files must not escalate approval, network, provider credentials, or the system prompt. */
47
55
  export function sanitizeWorkspaceConfig(value) {
48
- const provider = value.provider === undefined ? undefined : {
49
- ...(value.provider.provider === undefined ? {} : { provider: value.provider.provider }),
50
- ...(value.provider.model === undefined ? {} : { model: value.provider.model }),
51
- };
56
+ const providers = value.providers === undefined ? undefined : Object.fromEntries(Object.entries(value.providers).flatMap(([name, profile]) => {
57
+ const safe = {
58
+ provider: profile.provider,
59
+ ...(profile.name === undefined ? {} : { name: profile.name }),
60
+ };
61
+ return Object.keys(safe).length ? [[name, safe]] : [];
62
+ }));
52
63
  return {
53
64
  ...(value.cwd === undefined ? {} : { cwd: value.cwd }),
54
65
  ...(value.dataDir === undefined ? {} : { dataDir: value.dataDir }),
55
- ...(provider === undefined || Object.keys(provider).length === 0 ? {} : { provider }),
66
+ ...(providers === undefined || Object.keys(providers).length === 0 ? {} : { providers }),
67
+ ...(value.activeModel === undefined ? {} : { activeModel: value.activeModel }),
68
+ ...(value.models === undefined ? {} : {
69
+ models: Object.fromEntries(Object.entries(value.models).flatMap(([name, model]) => {
70
+ if (!model.provider || !model.modelCode)
71
+ return [];
72
+ return [[name, {
73
+ provider: model.provider,
74
+ modelCode: model.modelCode,
75
+ displayName: model.displayName,
76
+ ...(model.maxContextTokens === undefined ? {} : { maxContextTokens: model.maxContextTokens }),
77
+ ...(model.maxOutputTokens === undefined ? {} : { maxOutputTokens: model.maxOutputTokens }),
78
+ ...(model.reasoningEffort === undefined ? {} : { reasoningEffort: model.reasoningEffort }),
79
+ }]];
80
+ })),
81
+ }),
56
82
  };
57
83
  }
58
84
  export function mergePulseConfigs(layers) {
@@ -62,7 +88,8 @@ export function mergePulseConfigs(layers) {
62
88
  merged = {
63
89
  ...merged,
64
90
  ...value,
65
- ...(merged.provider === undefined && value.provider === undefined ? {} : { provider: { ...merged.provider, ...value.provider } }),
91
+ ...(merged.providers === undefined && value.providers === undefined ? {} : { providers: { ...merged.providers, ...value.providers } }),
92
+ ...(merged.models === undefined && value.models === undefined ? {} : { models: { ...merged.models, ...value.models } }),
66
93
  };
67
94
  }
68
95
  return merged;
@@ -11,5 +11,6 @@ export declare function useConversation({ host, conversationId, }: {
11
11
  addAssistantMessage: (msg: DisplayMessage) => void;
12
12
  clearMessages: () => void;
13
13
  switchConversation: (id: string) => Promise<void>;
14
- newConversation: () => Promise<void>;
14
+ newConversation: () => Promise<ConversationHandle | null>;
15
+ createConversation: () => Promise<ConversationHandle | null>;
15
16
  };
@@ -36,9 +36,10 @@ export function useConversation({ host, conversationId, }) {
36
36
  }
37
37
  }
38
38
  else {
39
- const conv = await currentHost.createConversation();
40
- if (mounted && conv) {
41
- setConversation(conv);
39
+ // An interactive window starts without a persisted session. The
40
+ // first user message (or an explicit /new) creates one.
41
+ if (mounted) {
42
+ setConversation(null);
42
43
  setMessages([]);
43
44
  }
44
45
  }
@@ -89,19 +90,24 @@ export function useConversation({ host, conversationId, }) {
89
90
  setError(err instanceof Error ? err.message : String(err));
90
91
  }
91
92
  }, [host, loadMessages]);
92
- const newConversation = useCallback(async () => {
93
+ const createConversation = useCallback(async () => {
93
94
  if (!host)
94
- return;
95
+ return null;
95
96
  try {
96
97
  const conv = await host.createConversation();
97
98
  setConversation(conv);
98
99
  setMessages([]);
99
100
  setError(null);
101
+ return conv;
100
102
  }
101
103
  catch (err) {
102
104
  setError(err instanceof Error ? err.message : String(err));
105
+ return null;
103
106
  }
104
107
  }, [host]);
108
+ const newConversation = useCallback(async () => {
109
+ return createConversation();
110
+ }, [createConversation]);
105
111
  return {
106
112
  conversation,
107
113
  messages,
@@ -111,5 +117,6 @@ export function useConversation({ host, conversationId, }) {
111
117
  clearMessages,
112
118
  switchConversation,
113
119
  newConversation,
120
+ createConversation,
114
121
  };
115
122
  }
@@ -1,5 +1,5 @@
1
1
  import type { LocalHost } from '@hunterzhu/pulse-server';
2
- import type { ApprovalRequest, DisplayMessage } from '../types.js';
2
+ import type { ApprovalRequest, AskRequest, DisplayMessage, LaneDisplay } from '../types.js';
3
3
  export declare function describeFactStatus(data: unknown): string;
4
4
  export declare function useRun({ host, conversationId, addAssistantMessage, }: {
5
5
  host: LocalHost | null;
@@ -10,8 +10,12 @@ export declare function useRun({ host, conversationId, addAssistantMessage, }: {
10
10
  currentStep: string | null;
11
11
  error: string | null;
12
12
  approvalRequest: ApprovalRequest | null;
13
- sendMessage: (text: string) => Promise<void>;
14
- resumeActive: () => Promise<void>;
13
+ askRequest: AskRequest | null;
14
+ lanes: LaneDisplay[];
15
+ approvalSubmitting: boolean;
16
+ sendMessage: (text: string, targetConversationId?: string) => Promise<void>;
17
+ resumeActive: (targetConversationId?: string) => Promise<void>;
15
18
  approveAction: (effectId: string, approved: boolean, reason?: string) => Promise<void>;
19
+ replyAsk: (effectId: string, value: Record<string, unknown>) => Promise<void>;
16
20
  cancelRun: () => Promise<void>;
17
21
  };
@@ -1,6 +1,6 @@
1
1
  import { useState, useRef, useCallback, useEffect } from 'react';
2
2
  function toolStatus(value) {
3
- if (value === 'succeeded' || value === 'failed' || value === 'running')
3
+ if (value === 'succeeded' || value === 'failed' || value === 'running' || value === 'cancelled')
4
4
  return value;
5
5
  return 'running';
6
6
  }
@@ -40,6 +40,9 @@ export function useRun({ host, conversationId, addAssistantMessage, }) {
40
40
  const [currentStep, setCurrentStep] = useState(null);
41
41
  const [error, setError] = useState(null);
42
42
  const [approvalRequest, setApprovalRequest] = useState(null);
43
+ const [askRequest, setAskRequest] = useState(null);
44
+ const [approvalSubmitting, setApprovalSubmitting] = useState(false);
45
+ const [lanes, setLanes] = useState([]);
43
46
  const runRef = useRef(null);
44
47
  const consumeRun = useCallback(async (run) => {
45
48
  runRef.current = run;
@@ -51,8 +54,30 @@ export function useRun({ host, conversationId, addAssistantMessage, }) {
51
54
  toolCalls: [],
52
55
  runId: run.id,
53
56
  };
54
- addAssistantMessage({ ...assistantMessage, toolCalls: [] });
57
+ let assistantStarted = false;
58
+ const ensureAssistant = () => {
59
+ if (assistantStarted)
60
+ return;
61
+ assistantStarted = true;
62
+ addAssistantMessage({ ...assistantMessage, toolCalls: [] });
63
+ };
55
64
  for await (const event of run.events) {
65
+ if (event.type === 'notice') {
66
+ const data = event.data && typeof event.data === 'object' && !Array.isArray(event.data)
67
+ ? event.data
68
+ : {};
69
+ const text = typeof data.text === 'string' ? data.text : '';
70
+ if (text) {
71
+ addAssistantMessage({
72
+ id: `notice-${run.id}-${event.seq}`,
73
+ role: 'system',
74
+ text,
75
+ createdAt: new Date().toISOString(),
76
+ });
77
+ }
78
+ continue;
79
+ }
80
+ ensureAssistant();
56
81
  switch (event.type) {
57
82
  case 'text':
58
83
  assistantMessage.text += String(event.data ?? '');
@@ -88,6 +113,31 @@ export function useRun({ host, conversationId, addAssistantMessage, }) {
88
113
  const input = payload.input && typeof payload.input === 'object' && !Array.isArray(payload.input)
89
114
  ? payload.input
90
115
  : {};
116
+ if (input.kind === 'ask' && (input.type === 'choice' || input.type === 'multi' || input.type === 'input')) {
117
+ const options = Array.isArray(input.options)
118
+ ? input.options.flatMap((option) => {
119
+ if (!option || typeof option !== 'object' || Array.isArray(option))
120
+ return [];
121
+ const item = option;
122
+ return typeof item.label === 'string' && typeof item.value === 'string' ? [{ label: item.label, value: item.value }] : [];
123
+ })
124
+ : undefined;
125
+ setApprovalRequest(null);
126
+ setAskRequest({
127
+ effectId,
128
+ toolName: typeof input.toolName === 'string' ? input.toolName : `ask.${input.type}`,
129
+ type: input.type,
130
+ prompt: typeof input.prompt === 'string' ? input.prompt : '请输入你的回答。',
131
+ ...(options && options.length ? { options } : {}),
132
+ ...(typeof input.min === 'number' ? { min: input.min } : {}),
133
+ ...(typeof input.max === 'number' ? { max: input.max } : {}),
134
+ ...(typeof input.placeholder === 'string' ? { placeholder: input.placeholder } : {}),
135
+ ...(typeof input.defaultValue === 'string' ? { defaultValue: input.defaultValue } : {}),
136
+ });
137
+ setCurrentStep('等待你的回答...');
138
+ break;
139
+ }
140
+ setAskRequest(null);
91
141
  const tools = Array.isArray(input.tools)
92
142
  ? input.tools.flatMap((tool) => {
93
143
  if (!tool || typeof tool !== 'object' || Array.isArray(tool))
@@ -115,6 +165,25 @@ export function useRun({ host, conversationId, addAssistantMessage, }) {
115
165
  break;
116
166
  }
117
167
  case 'fact':
168
+ if (event.data && typeof event.data === 'object' && !Array.isArray(event.data)) {
169
+ const fact = event.data;
170
+ if (fact.type === 'lane.snapshot' && Array.isArray(fact.lanes)) {
171
+ setLanes(fact.lanes.flatMap((lane) => {
172
+ if (!lane || typeof lane !== 'object' || Array.isArray(lane))
173
+ return [];
174
+ const item = lane;
175
+ if (typeof item.id !== 'string' || typeof item.status !== 'string' || typeof item.goal !== 'string')
176
+ return [];
177
+ return [{
178
+ id: item.id,
179
+ status: item.status,
180
+ goal: item.goal,
181
+ ...(typeof item.activity === 'string' ? { activity: item.activity } : {}),
182
+ }];
183
+ }));
184
+ break;
185
+ }
186
+ }
118
187
  setCurrentStep(describeFactStatus(event.data));
119
188
  break;
120
189
  case 'error':
@@ -122,10 +191,25 @@ export function useRun({ host, conversationId, addAssistantMessage, }) {
122
191
  setIsRunning(false);
123
192
  setCurrentStep(null);
124
193
  setApprovalRequest(null);
194
+ setAskRequest(null);
195
+ setLanes([]);
125
196
  break;
126
197
  case 'complete':
127
198
  setIsRunning(false);
128
199
  setCurrentStep(null);
200
+ setApprovalRequest(null);
201
+ setAskRequest(null);
202
+ if (event.data && typeof event.data === 'object' && !Array.isArray(event.data)) {
203
+ const completion = event.data;
204
+ const completionError = completion.error;
205
+ if (completion.status === 'failed' && completionError && typeof completionError === 'object' && !Array.isArray(completionError)) {
206
+ const error = completionError;
207
+ setError(`${String(error.code ?? 'RUN_FAILED')}: ${String(error.message ?? '运行失败')}`);
208
+ }
209
+ else if (completion.status === 'failed') {
210
+ setError('RUN_FAILED: 运行失败');
211
+ }
212
+ }
129
213
  break;
130
214
  }
131
215
  }
@@ -134,10 +218,12 @@ export function useRun({ host, conversationId, addAssistantMessage, }) {
134
218
  setIsRunning(false);
135
219
  setCurrentStep(null);
136
220
  setApprovalRequest(null);
221
+ setAskRequest(null);
137
222
  runRef.current = null;
138
223
  }, []);
139
- const sendMessage = useCallback(async (text) => {
140
- if (!host || !conversationId)
224
+ const sendMessage = useCallback(async (text, targetConversationId) => {
225
+ const activeConversationId = targetConversationId ?? conversationId;
226
+ if (!host || !activeConversationId)
141
227
  return;
142
228
  if (runRef.current) {
143
229
  try {
@@ -154,7 +240,7 @@ export function useRun({ host, conversationId, addAssistantMessage, }) {
154
240
  setError(null);
155
241
  setCurrentStep('思考中...');
156
242
  try {
157
- const run = await host.sendMessage(conversationId, { text });
243
+ const run = await host.sendMessage(activeConversationId, { text });
158
244
  await consumeRun(run);
159
245
  }
160
246
  catch (e) {
@@ -164,14 +250,15 @@ export function useRun({ host, conversationId, addAssistantMessage, }) {
164
250
  finishRun();
165
251
  }
166
252
  }, [host, conversationId, consumeRun, finishRun]);
167
- const resumeActive = useCallback(async () => {
168
- if (!host || !conversationId || runRef.current)
253
+ const resumeActive = useCallback(async (targetConversationId) => {
254
+ const activeConversationId = targetConversationId ?? conversationId;
255
+ if (!host || !activeConversationId || runRef.current)
169
256
  return;
170
257
  setIsRunning(true);
171
258
  setError(null);
172
259
  setCurrentStep('正在恢复未完成的运行...');
173
260
  try {
174
- const run = await host.resumeRun(conversationId);
261
+ const run = await host.resumeRun(activeConversationId);
175
262
  await consumeRun(run);
176
263
  }
177
264
  catch (e) {
@@ -187,12 +274,41 @@ export function useRun({ host, conversationId, addAssistantMessage, }) {
187
274
  void run.cancel('USER_INTERRUPT');
188
275
  }, []);
189
276
  const approveAction = useCallback(async (effectId, approved, reason) => {
190
- if (!effectId || !runRef.current)
277
+ if (!effectId || !runRef.current || approvalSubmitting)
191
278
  return;
192
- await runRef.current.reply(effectId, { approved, ...(approved ? {} : { reason: reason || '拒绝执行' }) });
193
- setApprovalRequest(null);
194
- setCurrentStep('审批已提交,正在继续...');
195
- }, []);
279
+ setApprovalSubmitting(true);
280
+ try {
281
+ await runRef.current.reply(effectId, { approved, ...(approved ? {} : { reason: reason || '拒绝执行' }) });
282
+ setApprovalRequest(null);
283
+ setCurrentStep('审批已提交,正在继续...');
284
+ setError(null);
285
+ }
286
+ catch (e) {
287
+ setError(`审批提交失败: ${e instanceof Error ? e.message : String(e)}`);
288
+ setCurrentStep('审批仍在等待,请重新选择。');
289
+ }
290
+ finally {
291
+ setApprovalSubmitting(false);
292
+ }
293
+ }, [approvalSubmitting]);
294
+ const replyAsk = useCallback(async (effectId, value) => {
295
+ if (!effectId || !runRef.current || approvalSubmitting)
296
+ return;
297
+ setApprovalSubmitting(true);
298
+ try {
299
+ await runRef.current.reply(effectId, value);
300
+ setAskRequest(null);
301
+ setCurrentStep('回答已提交,正在继续...');
302
+ setError(null);
303
+ }
304
+ catch (e) {
305
+ setError(`回答提交失败: ${e instanceof Error ? e.message : String(e)}`);
306
+ setCurrentStep('仍在等待你的回答,请重新选择。');
307
+ }
308
+ finally {
309
+ setApprovalSubmitting(false);
310
+ }
311
+ }, [approvalSubmitting]);
196
312
  const cancelRun = useCallback(async () => {
197
313
  if (runRef.current) {
198
314
  await runRef.current.cancel('USER_CANCELLED');
@@ -205,9 +321,13 @@ export function useRun({ host, conversationId, addAssistantMessage, }) {
205
321
  currentStep,
206
322
  error,
207
323
  approvalRequest,
324
+ askRequest,
325
+ lanes,
326
+ approvalSubmitting,
208
327
  sendMessage,
209
328
  resumeActive,
210
329
  approveAction,
330
+ replyAsk,
211
331
  cancelRun,
212
332
  };
213
333
  }
@@ -8,6 +8,7 @@ export interface SlashCommandDependencies {
8
8
  onQuit?: () => void | Promise<void>;
9
9
  onCancel?: () => void | Promise<void>;
10
10
  onNew?: () => void | Promise<void>;
11
+ onResume?: () => void | Promise<void>;
11
12
  onSessions?: () => void | Promise<void>;
12
13
  onDelete?: (args: string) => void | Promise<void>;
13
14
  onExport?: (args: string) => void | Promise<void>;
@@ -9,6 +9,7 @@ export function useSlashCommands(deps) {
9
9
  { name: '/quit', aliases: ['/q'], description: '退出程序', execute: async () => deps.onQuit?.() },
10
10
  { name: '/cancel', aliases: ['/stop'], description: '取消当前运行(保留会话)', execute: async () => deps.onCancel?.() },
11
11
  { name: '/new', description: '开启新会话', execute: async () => deps.onNew?.() },
12
+ { name: '/resume', description: '恢复上一次会话或未完成运行', execute: async () => deps.onResume?.() },
12
13
  { name: '/sessions', description: '查看所有会话', execute: async () => deps.onSessions?.() },
13
14
  { name: '/delete', description: '删除会话 [id]', execute: async (args) => deps.onDelete?.(args) },
14
15
  { name: '/export', description: '导出会话 [markdown|json]', execute: async (args) => deps.onExport?.(args) },
package/dist/types.d.ts CHANGED
@@ -31,9 +31,16 @@ export interface ToolCallDisplay {
31
31
  name: string;
32
32
  arguments?: Record<string, unknown> | undefined;
33
33
  result?: unknown;
34
- status: 'running' | 'succeeded' | 'failed';
34
+ status: 'running' | 'succeeded' | 'failed' | 'cancelled';
35
35
  durationMs?: number | undefined;
36
36
  }
37
+ /** Runtime lane snapshot rendered in the bottom status HUD. */
38
+ export interface LaneDisplay {
39
+ id: string;
40
+ status: string;
41
+ goal: string;
42
+ activity?: string | undefined;
43
+ }
37
44
  /** Token 统计数据 */
38
45
  export interface TokenStatsData {
39
46
  inputTokens: number;
@@ -55,6 +62,21 @@ export interface ApprovalRequest {
55
62
  input: Record<string, unknown>;
56
63
  }> | undefined;
57
64
  }
65
+ export type AskType = 'choice' | 'multi' | 'input';
66
+ export interface AskRequest {
67
+ effectId: string;
68
+ toolName: string;
69
+ type: AskType;
70
+ prompt: string;
71
+ options?: Array<{
72
+ label: string;
73
+ value: string;
74
+ }> | undefined;
75
+ min?: number | undefined;
76
+ max?: number | undefined;
77
+ placeholder?: string | undefined;
78
+ defaultValue?: string | undefined;
79
+ }
58
80
  /** 应用状态模式 */
59
81
  export type AppMode = 'chat' | 'sessions' | 'help' | 'config';
60
82
  /** 解析后的命令行参数 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunterzhu/pulse-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zhuhengtan/Pulse"
@@ -19,7 +19,7 @@
19
19
  "build": "tsc -p tsconfig.json"
20
20
  },
21
21
  "dependencies": {
22
- "@hunterzhu/pulse-server": "0.1.6",
22
+ "@hunterzhu/pulse-server": "0.1.7",
23
23
  "ink": "^7.1.1",
24
24
  "react": "^19.0.0",
25
25
  "ink-spinner": "^5.0.0",