Skip to content

Commit ffc6008

Browse files
author
huzijie.sea
committed
feat(command): 添加待处理命令队列功能
实现命令队列机制,允许在命令处理期间排队后续命令 - 添加 pendingCommands 状态和队列操作方法 - 修改 useCommandHandler 处理队列逻辑 - 在 MessageArea 显示待处理命令 - 移除不必要的输入禁用状态 - 扩展文件补全支持目录类型
1 parent 661cb51 commit ffc6008

10 files changed

Lines changed: 178 additions & 31 deletions

File tree

src/slash-commands/init.ts

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
*/
55

66
import { promises as fs } from 'fs';
7+
import type { ChatCompletionMessageToolCall } from 'openai/resources/chat';
78
import * as path from 'path';
89
import { Agent } from '../agent/Agent.js';
910
import { getState, sessionActions } from '../store/vanilla.js';
11+
import type { ToolResult } from '../tools/types/index.js';
12+
import { formatToolCallSummary } from '../ui/utils/toolFormatters.js';
1013
import type { SlashCommand, SlashCommandContext, SlashCommandResult } from './types.js';
1114

1215
const initCommand: SlashCommand = {
@@ -20,6 +23,7 @@ const initCommand: SlashCommand = {
2023
try {
2124
const { cwd } = context;
2225
const addMessage = sessionActions().addAssistantMessage;
26+
const addToolMessage = sessionActions().addToolMessage;
2327

2428
// 从 store 获取 sessionId
2529
const sessionId = getState().session.sessionId;
@@ -79,12 +83,42 @@ const initCommand: SlashCommand = {
7983
**Final output**: Return your analysis and suggestions as plain text. Do NOT use Write tool.`;
8084

8185
// 使用 chat 方法让 Agent 可以调用工具
82-
const result = await agent.chat(analysisPrompt, {
83-
messages: [],
84-
userId: 'cli-user',
85-
sessionId: sessionId || 'init-session',
86-
workspaceRoot: cwd,
87-
});
86+
const result = await agent.chat(
87+
analysisPrompt,
88+
{
89+
messages: [],
90+
userId: 'cli-user',
91+
sessionId: sessionId || 'init-session',
92+
workspaceRoot: cwd,
93+
},
94+
{
95+
onToolStart: (toolCall: ChatCompletionMessageToolCall) => {
96+
if (toolCall.type !== 'function') return;
97+
try {
98+
const params = JSON.parse(toolCall.function.arguments);
99+
const summary = formatToolCallSummary(toolCall.function.name, params);
100+
addToolMessage(summary, {
101+
toolName: toolCall.function.name,
102+
phase: 'start',
103+
summary,
104+
params,
105+
});
106+
} catch {
107+
// 静默处理解析错误
108+
}
109+
},
110+
onToolResult: async (toolCall: ChatCompletionMessageToolCall, result: ToolResult) => {
111+
if (toolCall.type !== 'function') return;
112+
if (result?.metadata?.summary) {
113+
addToolMessage(result.metadata.summary, {
114+
toolName: toolCall.function.name,
115+
phase: 'complete',
116+
summary: result.metadata.summary,
117+
});
118+
}
119+
},
120+
}
121+
);
88122

89123
addMessage(result);
90124

@@ -138,12 +172,42 @@ const initCommand: SlashCommand = {
138172
**Final output**: Return ONLY the complete BLADE.md content (markdown format), ready to be written to the file.`;
139173

140174
// 使用 chat 方法让 Agent 可以调用工具
141-
const generatedContent = await agent.chat(analysisPrompt, {
142-
messages: [],
143-
userId: 'cli-user',
144-
sessionId: sessionId || 'init-session',
145-
workspaceRoot: cwd,
146-
});
175+
const generatedContent = await agent.chat(
176+
analysisPrompt,
177+
{
178+
messages: [],
179+
userId: 'cli-user',
180+
sessionId: sessionId || 'init-session',
181+
workspaceRoot: cwd,
182+
},
183+
{
184+
onToolStart: (toolCall: ChatCompletionMessageToolCall) => {
185+
if (toolCall.type !== 'function') return;
186+
try {
187+
const params = JSON.parse(toolCall.function.arguments);
188+
const summary = formatToolCallSummary(toolCall.function.name, params);
189+
addToolMessage(summary, {
190+
toolName: toolCall.function.name,
191+
phase: 'start',
192+
summary,
193+
params,
194+
});
195+
} catch {
196+
// 静默处理解析错误
197+
}
198+
},
199+
onToolResult: async (toolCall: ChatCompletionMessageToolCall, result: ToolResult) => {
200+
if (toolCall.type !== 'function') return;
201+
if (result?.metadata?.summary) {
202+
addToolMessage(result.metadata.summary, {
203+
toolName: toolCall.function.name,
204+
phase: 'complete',
205+
summary: result.metadata.summary,
206+
});
207+
}
208+
},
209+
}
210+
);
147211

148212
// 验证生成内容的有效性(至少应该有基本的标题和内容)
149213
if (!generatedContent || generatedContent.trim().length === 0) {

src/store/selectors/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,12 @@ export const useAbortController = () =>
292292
*/
293293
export const useCommandActions = () => useBladeStore((state) => state.command.actions);
294294

295+
/**
296+
* 获取待处理命令队列
297+
*/
298+
export const usePendingCommands = () =>
299+
useBladeStore((state) => state.command.pendingCommands);
300+
295301
/**
296302
* 派生选择器:是否可以中止
297303
*/

src/store/slices/commandSlice.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { BladeStore, CommandSlice, CommandState } from '../types.js';
1818
const initialCommandState: CommandState = {
1919
isProcessing: false,
2020
abortController: null,
21+
pendingCommands: [],
2122
};
2223

2324
/**
@@ -66,6 +67,7 @@ export const createCommandSlice: StateCreator<
6667
* - 发送 abort signal
6768
* - 重置 isProcessing
6869
* - 重置 isThinking (跨 slice)
70+
* - 清空待处理队列
6971
*/
7072
abort: () => {
7173
const { abortController } = get().command;
@@ -78,12 +80,57 @@ export const createCommandSlice: StateCreator<
7880
// 重置 session 的 isThinking 状态
7981
get().session.actions.setThinking(false);
8082

81-
// 重置 command 状态
83+
// 重置 command 状态并清空队列
8284
set((state) => ({
8385
command: {
8486
...state.command,
8587
isProcessing: false,
8688
abortController: null,
89+
pendingCommands: [],
90+
},
91+
}));
92+
},
93+
94+
/**
95+
* 将命令加入待处理队列
96+
*/
97+
enqueueCommand: (command: string) => {
98+
set((state) => ({
99+
command: {
100+
...state.command,
101+
pendingCommands: [...state.command.pendingCommands, command],
102+
},
103+
}));
104+
},
105+
106+
/**
107+
* 从队列取出下一个命令
108+
*/
109+
dequeueCommand: () => {
110+
const { pendingCommands } = get().command;
111+
if (pendingCommands.length === 0) {
112+
return undefined;
113+
}
114+
115+
const [nextCommand, ...rest] = pendingCommands;
116+
set((state) => ({
117+
command: {
118+
...state.command,
119+
pendingCommands: rest,
120+
},
121+
}));
122+
123+
return nextCommand;
124+
},
125+
126+
/**
127+
* 清空待处理队列
128+
*/
129+
clearQueue: () => {
130+
set((state) => ({
131+
command: {
132+
...state.command,
133+
pendingCommands: [],
87134
},
88135
}));
89136
},

src/store/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ export interface FocusSlice extends FocusState {
227227
export interface CommandState {
228228
isProcessing: boolean; // 临时状态 - 不持久化
229229
abortController: AbortController | null; // 不持久化
230+
pendingCommands: string[]; // 待处理命令队列 - 不持久化
230231
}
231232

232233
/**
@@ -237,6 +238,9 @@ export interface CommandActions {
237238
createAbortController: () => AbortController;
238239
clearAbortController: () => void;
239240
abort: () => void;
241+
enqueueCommand: (command: string) => void;
242+
dequeueCommand: () => string | undefined;
243+
clearQueue: () => void;
240244
}
241245

242246
/**

src/ui/components/BladeInterface.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -503,8 +503,6 @@ export const BladeInterface: React.FC<BladeInterfaceProps> = ({
503503
/>
504504
) : null;
505505

506-
const isInputDisabled = isThinking || !readyForChat || inlineModelUiVisible;
507-
508506
return (
509507
<Box flexDirection="column" width="100%" height="100%">
510508
{blockingModal ?? (
@@ -520,7 +518,6 @@ export const BladeInterface: React.FC<BladeInterfaceProps> = ({
520518
cursorPosition={inputBuffer.cursorPosition}
521519
onChange={inputBuffer.setValue}
522520
onChangeCursorPosition={inputBuffer.setCursorPosition}
523-
isProcessing={isInputDisabled}
524521
/>
525522

526523
{inlineModelSelectorVisible && (

src/ui/components/InputArea.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { CustomTextInput } from './CustomTextInput.js';
99
interface InputAreaProps {
1010
input: string;
1111
cursorPosition: number;
12-
isProcessing: boolean;
1312
onChange: (value: string) => void;
1413
onChangeCursorPosition: (position: number) => void;
1514
}
@@ -20,14 +19,11 @@ interface InputAreaProps {
2019
* 注意:加载动画已移至 LoadingIndicator 组件,显示在输入框上方
2120
*/
2221
export const InputArea: React.FC<InputAreaProps> = React.memo(
23-
({ input, cursorPosition, isProcessing, onChange, onChangeCursorPosition }) => {
22+
({ input, cursorPosition, onChange, onChangeCursorPosition }) => {
2423
// 使用 Zustand store 管理焦点
2524
const currentFocus = useCurrentFocus();
2625
const isFocused = currentFocus === FocusId.MAIN_INPUT;
2726

28-
// 处理中时,禁用输入框(移除焦点和光标)
29-
const isEnabled = !isProcessing && isFocused;
30-
3127
// 文本粘贴回调 - 处理大段文本粘贴
3228
const handlePaste = useMemoizedFn((text: string): { prompt?: string } => {
3329
const lineCount = text.split('\n').length;
@@ -122,7 +118,7 @@ export const InputArea: React.FC<InputAreaProps> = React.memo(
122118
onPaste={handlePaste}
123119
onImagePaste={handleImagePaste}
124120
placeholder=" 输入命令..."
125-
focus={isEnabled}
121+
focus={isFocused}
126122
disabledKeys={['upArrow', 'downArrow', 'tab', 'return', 'escape']}
127123
/>
128124
</Box>

src/ui/components/MessageArea.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { ReactNode, useMemo } from 'react';
33
import {
44
useIsThinking,
55
useMessages,
6+
usePendingCommands,
67
useShowTodoPanel,
78
useTodos,
89
} from '../../store/selectors/index.js';
@@ -35,6 +36,7 @@ export const MessageArea: React.FC = React.memo(() => {
3536
const isThinking = useIsThinking();
3637
const todos = useTodos();
3738
const showTodoPanel = useShowTodoPanel();
39+
const pendingCommands = usePendingCommands();
3840

3941
// 使用 useTerminalWidth hook 获取终端宽度
4042
const terminalWidth = useTerminalWidth();
@@ -107,6 +109,17 @@ export const MessageArea: React.FC = React.memo(() => {
107109
<TodoPanel todos={todos} visible={true} compact={false} />
108110
</Box>
109111
)}
112+
113+
{/* 待处理命令队列(显示在最底部,作为下一轮对话的开始) */}
114+
{pendingCommands.map((cmd, index) => (
115+
<Box key={`pending-${index}`} flexDirection="column">
116+
<MessageRenderer
117+
content={cmd}
118+
role="user"
119+
terminalWidth={terminalWidth}
120+
/>
121+
</Box>
122+
))}
110123
</Box>
111124
</Box>
112125
);

src/ui/hooks/useAtCompletion.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,8 @@ export function useAtCompletion(
203203
cwd,
204204
dot: false,
205205
followSymbolicLinks: false,
206-
onlyFiles: true,
206+
onlyFiles: false,
207+
markDirectories: true,
207208
unique: true,
208209
ignore: ignorePatterns,
209210
})) as string[];

src/ui/hooks/useCommandHandler.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useMemoizedFn } from 'ahooks';
2+
import type { ChatCompletionMessageToolCall } from 'openai/resources/chat';
23
import { useEffect, useRef } from 'react';
34
import { createLogger, LogCategory } from '../../logging/Logger.js';
45
import type { SessionMetadata } from '../../services/SessionService.js';
@@ -18,6 +19,7 @@ import {
1819
} from '../../store/selectors/index.js';
1920
import { ensureStoreInitialized } from '../../store/vanilla.js';
2021
import type { ConfirmationHandler } from '../../tools/types/ExecutionTypes.js';
22+
import type { ToolResult } from '../../tools/types/index.js';
2123
import {
2224
formatToolCallSummary,
2325
shouldShowToolDetail,
@@ -60,6 +62,9 @@ function handleSlashMessage(
6062
appActions.showSessionSelector(sessions);
6163
return true;
6264
}
65+
case 'exit_application':
66+
process.exit(0);
67+
return true;
6368
default:
6469
return false;
6570
}
@@ -218,7 +223,8 @@ export const useCommandHandler = (
218223
}
219224
},
220225
// 工具调用开始
221-
onToolStart: (toolCall: any) => {
226+
onToolStart: (toolCall: ChatCompletionMessageToolCall) => {
227+
if (toolCall.type !== 'function') return;
222228
// 跳过 TodoWrite 的显示(任务列表由侧边栏显示)
223229
if (toolCall.function.name === 'TodoWrite') {
224230
return;
@@ -238,7 +244,11 @@ export const useCommandHandler = (
238244
}
239245
},
240246
// 工具执行完成(显示摘要 + 可选的详细内容)
241-
onToolResult: async (toolCall: any, result: any) => {
247+
onToolResult: async (
248+
toolCall: ChatCompletionMessageToolCall,
249+
result: ToolResult
250+
) => {
251+
if (toolCall.type !== 'function') return;
242252
if (!result?.metadata?.summary) {
243253
return;
244254
}
@@ -295,12 +305,14 @@ export const useCommandHandler = (
295305
return;
296306
}
297307

308+
const trimmedCommand = command.trim();
309+
310+
// 如果正在处理,静默加入队列(执行时再显示用户消息)
298311
if (isProcessing) {
312+
commandActions.enqueueCommand(trimmedCommand);
299313
return;
300314
}
301315

302-
const trimmedCommand = command.trim();
303-
304316
// 清空上一轮对话的 todos
305317
appActions.setTodos([]);
306318

@@ -333,6 +345,13 @@ export const useCommandHandler = (
333345
commandActions.setProcessing(false);
334346
sessionActions.setThinking(false);
335347
commandActions.clearAbortController();
348+
349+
// 处理队列中的下一个命令
350+
const nextCommand = commandActions.dequeueCommand();
351+
if (nextCommand) {
352+
// 稍微延迟以让 UI 更新
353+
setTimeout(() => executeCommand(nextCommand), 100);
354+
}
336355
}
337356
});
338357

0 commit comments

Comments
 (0)