Skip to content

Commit afb11a3

Browse files
author
huzijie.sea
committed
feat: add thinking block UI and model detection, enhance chat features
- Add ThinkingBlock component for displaying reasoning process - Add model detection utility for provider-specific features - Enhance OpenAI service with reasoning support - Update store to manage thinking states - Improve command and input handling - Update UI components for better message display
1 parent 107d074 commit afb11a3

15 files changed

Lines changed: 423 additions & 24 deletions

File tree

.blade/settings.local.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
{
22
"permissions": {
33
"allow": [
4-
"TestTool"
4+
"TestTool",
5+
"Bash(git status)",
6+
"Bash(git diff *)"
57
],
68
"ask": [],
79
"deny": []

src/agent/Agent.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,11 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
669669
logger.debug('当前权限模式:', context.permissionMode);
670670
logger.debug('================================\n');
671671

672+
// 🆕 如果 LLM 返回了 thinking 内容(DeepSeek R1 等),通知 UI
673+
if (turnResult.reasoningContent && options?.onThinking) {
674+
options.onThinking(turnResult.reasoningContent);
675+
}
676+
672677
// 🆕 如果 LLM 返回了 content,立即显示
673678
if (turnResult.content && turnResult.content.trim() && options?.onContent) {
674679
options.onContent(turnResult.content);

src/config/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export interface ModelConfig {
6161
maxContextTokens?: number; // 上下文窗口大小
6262
topP?: number;
6363
topK?: number;
64+
65+
// Thinking 模型配置(如 DeepSeek R1)
66+
supportsThinking?: boolean; // 手动覆盖自动检测结果
67+
thinkingBudget?: number; // 思考 token 预算(可选)
6468
}
6569

6670
export interface BladeConfig {

src/services/ChatServiceInterface.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ export interface ChatConfig {
4141
*/
4242
export interface ChatResponse {
4343
content: string;
44+
reasoningContent?: string; // Thinking 模型的推理过程(如 DeepSeek R1)
4445
toolCalls?: ChatCompletionMessageToolCall[];
4546
usage?: {
4647
promptTokens: number;
4748
completionTokens: number;
4849
totalTokens: number;
50+
reasoningTokens?: number; // Thinking 模型消耗的推理 tokens
4951
};
5052
}
5153

@@ -54,6 +56,7 @@ export interface ChatResponse {
5456
*/
5557
export interface StreamChunk {
5658
content?: string;
59+
reasoningContent?: string; // Thinking 模型的推理过程片段
5760
// biome-ignore lint/suspicious/noExplicitAny: 不同 provider 的 tool call 类型不同
5861
toolCalls?: any[];
5962
finishReason?: string;

src/services/OpenAIChatService.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,26 @@ export class OpenAIChatService implements IChatService {
193193
(tc): tc is ChatCompletionMessageToolCall => tc.type === 'function'
194194
);
195195

196+
// 提取 reasoning_content(DeepSeek R1 等 thinking 模型的扩展字段)
197+
const extendedMessage = choice.message as typeof choice.message & {
198+
reasoning_content?: string;
199+
};
200+
const reasoningContent = extendedMessage.reasoning_content || undefined;
201+
202+
// 提取 reasoning_tokens(thinking 模型的扩展 usage 字段)
203+
const extendedUsage = completion.usage as typeof completion.usage & {
204+
reasoning_tokens?: number;
205+
};
206+
196207
const response = {
197208
content: choice.message.content || '',
209+
reasoningContent,
198210
toolCalls: toolCalls,
199211
usage: {
200212
promptTokens: completion.usage?.prompt_tokens || 0,
201213
completionTokens: completion.usage?.completion_tokens || 0,
202214
totalTokens: completion.usage?.total_tokens || 0,
215+
reasoningTokens: extendedUsage?.reasoning_tokens,
203216
},
204217
};
205218

@@ -309,6 +322,7 @@ export class OpenAIChatService implements IChatService {
309322

310323
let chunkCount = 0;
311324
let totalContent = '';
325+
let totalReasoningContent = '';
312326
let toolCallsReceived = false;
313327

314328
for await (const chunk of stream) {
@@ -326,10 +340,19 @@ export class OpenAIChatService implements IChatService {
326340
continue;
327341
}
328342

343+
// 提取 reasoning_content(DeepSeek R1 等 thinking 模型的扩展字段)
344+
const extendedDelta = delta as typeof delta & {
345+
reasoning_content?: string;
346+
};
347+
329348
if (delta.content) {
330349
totalContent += delta.content;
331350
}
332351

352+
if (extendedDelta.reasoning_content) {
353+
totalReasoningContent += extendedDelta.reasoning_content;
354+
}
355+
333356
if (delta.tool_calls && !toolCallsReceived) {
334357
toolCallsReceived = true;
335358
_logger.debug('🔧 [ChatService] Tool calls detected in stream');
@@ -341,13 +364,15 @@ export class OpenAIChatService implements IChatService {
341364
_logger.debug('📊 [ChatService] Stream summary:', {
342365
totalChunks: chunkCount,
343366
totalContentLength: totalContent.length,
367+
totalReasoningContentLength: totalReasoningContent.length,
344368
hadToolCalls: toolCallsReceived,
345369
duration: Date.now() - startTime + 'ms',
346370
});
347371
}
348372

349373
yield {
350374
content: delta.content || undefined,
375+
reasoningContent: extendedDelta.reasoning_content || undefined,
351376
toolCalls: delta.tool_calls,
352377
finishReason: finishReason || undefined,
353378
};

src/store/selectors/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,23 @@ export const useIsModal = (modal: ActiveModal) =>
342342
*/
343343
export const useIsBusy = () =>
344344
useBladeStore((state) => state.session.isThinking || state.command.isProcessing);
345+
346+
// ==================== Thinking 模式选择器 ====================
347+
348+
/**
349+
* 获取 Thinking 模式是否启用
350+
*/
351+
export const useThinkingModeEnabled = () =>
352+
useBladeStore((state) => state.app.thinkingModeEnabled);
353+
354+
/**
355+
* 获取当前 Thinking 内容(流式接收中)
356+
*/
357+
export const useCurrentThinkingContent = () =>
358+
useBladeStore((state) => state.session.currentThinkingContent);
359+
360+
/**
361+
* 获取 Thinking 内容是否展开
362+
*/
363+
export const useThinkingExpanded = () =>
364+
useBladeStore((state) => state.session.thinkingExpanded);

src/store/slices/appSlice.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const initialAppState: AppState = {
3232
modelEditorTarget: null,
3333
todos: [],
3434
awaitingSecondCtrlC: false,
35+
thinkingModeEnabled: false, // Thinking 模式默认关闭
3536
};
3637

3738
/**
@@ -137,5 +138,25 @@ export const createAppSlice: StateCreator<BladeStore, [], [], AppSlice> = (set)
137138
app: { ...state.app, awaitingSecondCtrlC: awaiting },
138139
}));
139140
},
141+
142+
// ==================== Thinking 模式相关 actions ====================
143+
144+
/**
145+
* 设置 Thinking 模式开关状态
146+
*/
147+
setThinkingModeEnabled: (enabled: boolean) => {
148+
set((state) => ({
149+
app: { ...state.app, thinkingModeEnabled: enabled },
150+
}));
151+
},
152+
153+
/**
154+
* 切换 Thinking 模式开关
155+
*/
156+
toggleThinkingMode: () => {
157+
set((state) => ({
158+
app: { ...state.app, thinkingModeEnabled: !state.app.thinkingModeEnabled },
159+
}));
160+
},
140161
},
141162
});

src/store/slices/sessionSlice.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const initialSessionState: SessionState = {
4141
error: null,
4242
isActive: true,
4343
tokenUsage: { ...initialTokenUsage },
44+
currentThinkingContent: null,
45+
thinkingExpanded: false,
4446
};
4547

4648
/**
@@ -83,13 +85,16 @@ export const createSessionSlice: StateCreator<
8385

8486
/**
8587
* 添加助手消息
88+
* @param content 消息内容
89+
* @param thinkingContent 可选的 thinking 内容(如 DeepSeek R1 的推理过程)
8690
*/
87-
addAssistantMessage: (content: string) => {
91+
addAssistantMessage: (content: string, thinkingContent?: string) => {
8892
const message: SessionMessage = {
8993
id: `assistant-${Date.now()}-${Math.random()}`,
9094
role: 'assistant',
9195
content,
9296
timestamp: Date.now(),
97+
thinkingContent,
9398
};
9499
get().session.actions.addMessage(message);
95100
},
@@ -208,5 +213,50 @@ export const createSessionSlice: StateCreator<
208213
},
209214
}));
210215
},
216+
217+
// ==================== Thinking 相关 actions ====================
218+
219+
/**
220+
* 设置当前 thinking 内容(用于流式接收)
221+
*/
222+
setCurrentThinkingContent: (content: string | null) => {
223+
set((state) => ({
224+
session: { ...state.session, currentThinkingContent: content },
225+
}));
226+
},
227+
228+
/**
229+
* 追加 thinking 内容(用于流式接收增量)
230+
*/
231+
appendThinkingContent: (delta: string) => {
232+
set((state) => ({
233+
session: {
234+
...state.session,
235+
currentThinkingContent:
236+
(state.session.currentThinkingContent || '') + delta,
237+
},
238+
}));
239+
},
240+
241+
/**
242+
* 设置 thinking 内容是否展开
243+
*/
244+
setThinkingExpanded: (expanded: boolean) => {
245+
set((state) => ({
246+
session: { ...state.session, thinkingExpanded: expanded },
247+
}));
248+
},
249+
250+
/**
251+
* 切换 thinking 内容展开/折叠状态
252+
*/
253+
toggleThinkingExpanded: () => {
254+
set((state) => ({
255+
session: {
256+
...state.session,
257+
thinkingExpanded: !state.session.thinkingExpanded,
258+
},
259+
}));
260+
},
211261
},
212262
});

src/store/types.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface SessionMessage {
4040
content: string;
4141
timestamp: number;
4242
metadata?: Record<string, unknown> | ToolMessageMetadata;
43+
thinkingContent?: string; // Thinking 模型的推理过程内容
4344
}
4445

4546
/**
@@ -64,6 +65,8 @@ export interface SessionState {
6465
error: string | null;
6566
isActive: boolean;
6667
tokenUsage: TokenUsage; // Token 使用量统计
68+
currentThinkingContent: string | null; // 当前正在接收的 thinking 内容(流式)
69+
thinkingExpanded: boolean; // thinking 内容是否展开显示
6770
}
6871

6972
/**
@@ -72,7 +75,7 @@ export interface SessionState {
7275
export interface SessionActions {
7376
addMessage: (message: SessionMessage) => void;
7477
addUserMessage: (content: string) => void;
75-
addAssistantMessage: (content: string) => void;
78+
addAssistantMessage: (content: string, thinkingContent?: string) => void;
7679
addToolMessage: (content: string, metadata?: ToolMessageMetadata) => void;
7780
setThinking: (isThinking: boolean) => void;
7881
setCompacting: (isCompacting: boolean) => void;
@@ -83,6 +86,11 @@ export interface SessionActions {
8386
restoreSession: (sessionId: string, messages: SessionMessage[]) => void;
8487
updateTokenUsage: (usage: Partial<TokenUsage>) => void;
8588
resetTokenUsage: () => void;
89+
// Thinking 相关 actions
90+
setCurrentThinkingContent: (content: string | null) => void;
91+
appendThinkingContent: (delta: string) => void;
92+
setThinkingExpanded: (expanded: boolean) => void;
93+
toggleThinkingExpanded: () => void;
8694
}
8795

8896
/**
@@ -155,6 +163,7 @@ export interface AppState {
155163
modelEditorTarget: ModelConfig | null;
156164
todos: TodoItem[];
157165
awaitingSecondCtrlC: boolean; // 是否等待第二次 Ctrl+C 退出
166+
thinkingModeEnabled: boolean; // Thinking 模式是否启用(Tab 切换)
158167
}
159168

160169
/**
@@ -170,6 +179,9 @@ export interface AppActions {
170179
setTodos: (todos: TodoItem[]) => void;
171180
updateTodo: (todo: TodoItem) => void;
172181
setAwaitingSecondCtrlC: (awaiting: boolean) => void;
182+
// Thinking 模式相关
183+
setThinkingModeEnabled: (enabled: boolean) => void;
184+
toggleThinkingMode: () => void;
173185
}
174186

175187
/**

src/ui/components/ChatStatusBar.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
useIsReady,
1111
useIsThinking,
1212
usePermissionMode,
13+
useThinkingModeEnabled,
1314
} from '../../store/selectors/index.js';
15+
import { isThinkingModel } from '../../utils/modelDetection.js';
1416
import { useGitBranch } from '../hooks/useGitBranch.js';
1517

1618
/**
@@ -32,6 +34,10 @@ export const ChatStatusBar: React.FC = React.memo(() => {
3234
const currentModel = useCurrentModel();
3335
const contextRemaining = useContextRemaining();
3436
const isCompacting = useIsCompacting();
37+
const thinkingModeEnabled = useThinkingModeEnabled();
38+
39+
// 检查当前模型是否支持 thinking
40+
const supportsThinking = currentModel ? isThinkingModel(currentModel) : false;
3541
// 渲染模式提示(仅非 DEFAULT 模式显示)
3642
const renderModeIndicator = () => {
3743
if (permissionMode === PermissionMode.DEFAULT) {
@@ -116,6 +122,17 @@ export const ChatStatusBar: React.FC = React.memo(() => {
116122
<Text color="red">⚠ API 密钥未配置</Text>
117123
) : (
118124
<>
125+
{/* Thinking 模式指示器(仅当模型支持时显示) */}
126+
{supportsThinking && (
127+
<>
128+
{thinkingModeEnabled ? (
129+
<Text color="cyan">Thinking on</Text>
130+
) : (
131+
<Text color="gray">Tab:Thinking</Text>
132+
)}
133+
<Text color="gray">·</Text>
134+
</>
135+
)}
119136
{currentModel && <Text color="gray">{currentModel.model}</Text>}
120137
<Text color="gray">·</Text>
121138
{isCompacting ? (

0 commit comments

Comments
 (0)