snow-ai 0.3.22 → 0.3.23

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.
@@ -7,7 +7,7 @@ export interface GeminiOptions {
7
7
  includeBuiltinSystemPrompt?: boolean;
8
8
  }
9
9
  export interface GeminiStreamChunk {
10
- type: 'content' | 'tool_calls' | 'tool_call_delta' | 'done' | 'usage';
10
+ type: 'content' | 'tool_calls' | 'tool_call_delta' | 'done' | 'usage' | 'reasoning_started' | 'reasoning_delta';
11
11
  content?: string;
12
12
  tool_calls?: Array<{
13
13
  id: string;
@@ -19,6 +19,10 @@ export interface GeminiStreamChunk {
19
19
  }>;
20
20
  delta?: string;
21
21
  usage?: UsageInfo;
22
+ thinking?: {
23
+ type: 'thinking';
24
+ thinking: string;
25
+ };
22
26
  }
23
27
  export declare function resetGeminiClient(): void;
24
28
  /**
@@ -17,6 +17,7 @@ function getGeminiConfig() {
17
17
  ? config.baseUrl
18
18
  : 'https://generativelanguage.googleapis.com/v1beta',
19
19
  customHeaders,
20
+ geminiThinking: config.geminiThinking,
20
21
  };
21
22
  }
22
23
  return geminiConfig;
@@ -230,10 +231,16 @@ export async function* createStreamingGeminiCompletion(options, abortSignal, onR
230
231
  systemInstruction: systemInstruction
231
232
  ? { parts: [{ text: systemInstruction }] }
232
233
  : undefined,
233
- generationConfig: {
234
- temperature: options.temperature ?? 0.7,
235
- },
236
234
  };
235
+ // Add thinking configuration if enabled
236
+ // Only include generationConfig when thinking is enabled
237
+ if (config.geminiThinking?.enabled) {
238
+ requestBody.generationConfig = {
239
+ thinkingConfig: {
240
+ thinkingBudget: config.geminiThinking.budget,
241
+ },
242
+ };
243
+ }
237
244
  // Add tools if provided
238
245
  const geminiTools = convertToolsToGemini(options.tools);
239
246
  if (geminiTools) {
@@ -263,6 +270,7 @@ export async function* createStreamingGeminiCompletion(options, abortSignal, onR
263
270
  throw new Error('No response body from Gemini API');
264
271
  }
265
272
  let contentBuffer = '';
273
+ let thinkingTextBuffer = ''; // Accumulate thinking text content
266
274
  let toolCallsBuffer = [];
267
275
  let hasToolCalls = false;
268
276
  let toolCallIndex = 0;
@@ -310,8 +318,17 @@ export async function* createStreamingGeminiCompletion(options, abortSignal, onR
310
318
  const candidate = chunk.candidates[0];
311
319
  if (candidate.content && candidate.content.parts) {
312
320
  for (const part of candidate.content.parts) {
313
- // Process text content
314
- if (part.text) {
321
+ // Process thought content (Gemini thinking)
322
+ // When part.thought === true, the text field contains thinking content
323
+ if (part.thought === true && part.text) {
324
+ thinkingTextBuffer += part.text;
325
+ yield {
326
+ type: 'reasoning_delta',
327
+ delta: part.text,
328
+ };
329
+ }
330
+ // Process regular text content (when thought is not true)
331
+ else if (part.text) {
315
332
  contentBuffer += part.text;
316
333
  yield {
317
334
  type: 'content',
@@ -374,9 +391,17 @@ export async function* createStreamingGeminiCompletion(options, abortSignal, onR
374
391
  usage: usageData,
375
392
  };
376
393
  }
394
+ // Return complete thinking block if thinking content exists
395
+ const thinkingBlock = thinkingTextBuffer
396
+ ? {
397
+ type: 'thinking',
398
+ thinking: thinkingTextBuffer,
399
+ }
400
+ : undefined;
377
401
  // Signal completion
378
402
  yield {
379
403
  type: 'done',
404
+ thinking: thinkingBlock,
380
405
  };
381
406
  }, {
382
407
  abortSignal,
@@ -79,6 +79,19 @@ function getOpenAIConfig() {
79
79
  }
80
80
  return openaiConfig;
81
81
  }
82
+ function getResponsesReasoningConfig() {
83
+ const config = getOpenAiConfig();
84
+ const reasoningConfig = config.responsesReasoning;
85
+ // 如果 reasoning 未启用,返回 null
86
+ if (!reasoningConfig?.enabled) {
87
+ return null;
88
+ }
89
+ // 返回配置,summary 永远默认为 'auto'
90
+ return {
91
+ effort: reasoningConfig.effort || 'high',
92
+ summary: 'auto',
93
+ };
94
+ }
82
95
  export function resetOpenAIClient() {
83
96
  openaiConfig = null;
84
97
  }
@@ -237,6 +250,8 @@ export async function* createStreamingResponse(options, abortSignal, onRetry) {
237
250
  const config = getOpenAIConfig();
238
251
  // 提取系统提示词和转换后的消息
239
252
  const { input: requestInput, systemInstructions } = convertToResponseInput(options.messages, options.includeBuiltinSystemPrompt !== false);
253
+ // 获取配置的 reasoning 设置
254
+ const configuredReasoning = getResponsesReasoningConfig();
240
255
  // 使用重试包装生成器
241
256
  yield* withRetryGenerator(async function* () {
242
257
  const requestPayload = {
@@ -246,9 +261,9 @@ export async function* createStreamingResponse(options, abortSignal, onRetry) {
246
261
  tools: convertToolsForResponses(options.tools),
247
262
  tool_choice: options.tool_choice,
248
263
  parallel_tool_calls: false,
249
- // Only add reasoning if not explicitly disabled (null means don't pass it)
250
- ...(options.reasoning !== null && {
251
- reasoning: options.reasoning || { effort: 'high', summary: 'auto' },
264
+ // 只有当 reasoning 启用时才添加 reasoning 字段
265
+ ...(configuredReasoning && {
266
+ reasoning: configuredReasoning,
252
267
  }),
253
268
  store: false,
254
269
  stream: true,
@@ -124,6 +124,7 @@ export async function handleConversationWithTools(options) {
124
124
  let receivedToolCalls;
125
125
  let receivedReasoning;
126
126
  let receivedThinking; // Accumulate thinking content from all platforms
127
+ let hasStartedReasoning = false; // Track if reasoning has started (for Gemini thinking)
127
128
  // Stream AI response - choose API based on config
128
129
  let toolCallAccumulator = ''; // Accumulate tool call deltas for token counting
129
130
  let reasoningAccumulator = ''; // Accumulate reasoning summary deltas for token counting (Responses API only)
@@ -194,6 +195,23 @@ export async function handleConversationWithTools(options) {
194
195
  // Reasoning started (Responses API only) - set reasoning state
195
196
  setIsReasoning?.(true);
196
197
  }
198
+ else if (chunk.type === 'reasoning_delta' && chunk.delta) {
199
+ // Handle reasoning delta from Gemini thinking
200
+ // When reasoning_delta is received, set reasoning state if not already set
201
+ if (!hasStartedReasoning) {
202
+ setIsReasoning?.(true);
203
+ hasStartedReasoning = true;
204
+ }
205
+ // Note: reasoning content is NOT sent back to AI, only counted for display
206
+ reasoningAccumulator += chunk.delta;
207
+ try {
208
+ const tokens = encoder.encode(streamedContent + toolCallAccumulator + reasoningAccumulator);
209
+ setStreamTokenCount(tokens.length);
210
+ }
211
+ catch (e) {
212
+ // Ignore encoding errors
213
+ }
214
+ }
197
215
  else if (chunk.type === 'content' && chunk.content) {
198
216
  // Accumulate content and update token count
199
217
  // When content starts, reasoning is done
@@ -220,18 +238,6 @@ export async function handleConversationWithTools(options) {
220
238
  // Ignore encoding errors
221
239
  }
222
240
  }
223
- else if (chunk.type === 'reasoning_delta' && chunk.delta) {
224
- // Accumulate reasoning summary deltas for token counting (Responses API only)
225
- // Note: reasoning content is NOT sent back to AI, only counted for display
226
- reasoningAccumulator += chunk.delta;
227
- try {
228
- const tokens = encoder.encode(streamedContent + toolCallAccumulator + reasoningAccumulator);
229
- setStreamTokenCount(tokens.length);
230
- }
231
- catch (e) {
232
- // Ignore encoding errors
233
- }
234
- }
235
241
  else if (chunk.type === 'tool_calls' && chunk.tool_calls) {
236
242
  receivedToolCalls = chunk.tool_calls;
237
243
  }
@@ -34,11 +34,18 @@ export function useHistoryNavigation(buffer, triggerUpdate, chatHistory, onHisto
34
34
  .map((msg, index) => ({ ...msg, originalIndex: index }))
35
35
  .filter(msg => msg.role === 'user' && msg.content.trim());
36
36
  // Keep original order (oldest first, newest last) and map with display numbers
37
- return userMessages.map((msg, index) => ({
38
- label: `${index + 1}. ${msg.content.slice(0, 50)}${msg.content.length > 50 ? '...' : ''}`,
39
- value: msg.originalIndex.toString(),
40
- infoText: msg.content,
41
- }));
37
+ return userMessages.map((msg, index) => {
38
+ // Remove all newlines, control characters and extra whitespace to ensure single line display
39
+ const cleanContent = msg.content
40
+ .replace(/[\r\n\t\v\f\u0000-\u001F\u007F-\u009F]+/g, ' ')
41
+ .replace(/\s+/g, ' ')
42
+ .trim();
43
+ return {
44
+ label: `${index + 1}. ${cleanContent.slice(0, 50)}${cleanContent.length > 50 ? '...' : ''}`,
45
+ value: msg.originalIndex.toString(),
46
+ infoText: msg.content,
47
+ };
48
+ });
42
49
  }, [chatHistory]);
43
50
  // Handle history selection
44
51
  const handleHistorySelect = useCallback((value) => {
@@ -71,7 +78,7 @@ export function useHistoryNavigation(buffer, triggerUpdate, chatHistory, onHisto
71
78
  triggerUpdate();
72
79
  }
73
80
  return true;
74
- }, [currentHistoryIndex, buffer]);
81
+ }, [currentHistoryIndex]); // 移除 buffer 避免循环依赖
75
82
  // Terminal-style history navigation: navigate down (newer)
76
83
  const navigateHistoryDown = useCallback(() => {
77
84
  if (currentHistoryIndex === -1)
@@ -93,7 +100,7 @@ export function useHistoryNavigation(buffer, triggerUpdate, chatHistory, onHisto
93
100
  }
94
101
  triggerUpdate();
95
102
  return true;
96
- }, [currentHistoryIndex, buffer]);
103
+ }, [currentHistoryIndex]); // 移除 buffer 避免循环依赖
97
104
  // Reset history navigation state
98
105
  const resetHistoryNavigation = useCallback(() => {
99
106
  setCurrentHistoryIndex(-1);
@@ -8,13 +8,14 @@ export function useInputBuffer(viewport) {
8
8
  const now = Date.now();
9
9
  lastUpdateTime.current = now;
10
10
  forceUpdate({});
11
- }, []);
11
+ }, []); // 空依赖项确保函数稳定
12
12
  const [buffer] = useState(() => new TextBuffer(viewport, triggerUpdate));
13
13
  // Update buffer viewport when viewport changes
14
14
  useEffect(() => {
15
15
  buffer.updateViewport(viewport);
16
- triggerUpdate();
17
- }, [viewport.width, viewport.height, buffer]);
16
+ // 直接调用 forceUpdate 而不是 triggerUpdate,避免依赖问题
17
+ forceUpdate({});
18
+ }, [viewport.width, viewport.height]); // 移除 buffer 和 triggerUpdate 避免循环依赖
18
19
  // Cleanup buffer on unmount
19
20
  useEffect(() => {
20
21
  return () => {
@@ -193,7 +193,7 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
193
193
  const percentage = calculateContextPercentage(contextUsage);
194
194
  onContextPercentageChange(percentage);
195
195
  }
196
- }, [contextUsage, onContextPercentageChange]);
196
+ }, [contextUsage]); // 移除 onContextPercentageChange 避免循环依赖
197
197
  // Render cursor based on focus state
198
198
  const renderCursor = useCallback((char) => {
199
199
  if (hasFocus) {
@@ -223,7 +223,7 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
223
223
  renderCursor(' '),
224
224
  React.createElement(Text, { color: disabled ? 'darkGray' : 'gray', dimColor: true }, disabled ? 'Waiting for response...' : placeholder)));
225
225
  }
226
- }, [buffer, disabled, placeholder, renderCursor, buffer.text]);
226
+ }, [buffer, disabled, placeholder, renderCursor]); // 移除 buffer.text 避免循环依赖,buffer 变化时会自然触发重渲染
227
227
  return (React.createElement(Box, { flexDirection: "column", paddingX: 1, width: terminalWidth },
228
228
  showHistoryMenu && (React.createElement(Box, { flexDirection: "column", marginBottom: 1, width: terminalWidth - 2 },
229
229
  React.createElement(Box, { flexDirection: "column" }, (() => {
@@ -248,8 +248,9 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
248
248
  " more above...")) : (React.createElement(Text, null, " "))),
249
249
  visibleMessages.map((message, displayIndex) => {
250
250
  const actualIndex = startIndex + displayIndex;
251
- // Remove all newlines and extra spaces from label to ensure single line
251
+ // Ensure single line by removing all newlines and control characters
252
252
  const singleLineLabel = message.label
253
+ .replace(/[\r\n\t\v\f\u0000-\u001F\u007F-\u009F]+/g, ' ')
253
254
  .replace(/\s+/g, ' ')
254
255
  .trim();
255
256
  // Calculate available width for the message
@@ -261,7 +262,7 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
261
262
  return (React.createElement(Box, { key: message.value, height: 1 },
262
263
  React.createElement(Text, { color: actualIndex === historySelectedIndex
263
264
  ? 'green'
264
- : 'white', bold: true },
265
+ : 'white', bold: true, wrap: "truncate" },
265
266
  actualIndex === historySelectedIndex ? '❯ ' : ' ',
266
267
  truncatedLabel)));
267
268
  }),
@@ -1,6 +1,6 @@
1
1
  import React, { useState, useEffect, useCallback } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
- import { sessionManager } from '../../utils/sessionManager.js';
3
+ import { sessionManager, } from '../../utils/sessionManager.js';
4
4
  export default function SessionListPanel({ onSelectSession, onClose }) {
5
5
  const [sessions, setSessions] = useState([]);
6
6
  const [loading, setLoading] = useState(true);
@@ -140,32 +140,36 @@ export default function SessionListPanel({ onSelectSession, onClose }) {
140
140
  sessions.length,
141
141
  ")",
142
142
  currentSession && ` • ${currentSession.messageCount} msgs`,
143
- markedSessions.size > 0 && React.createElement(Text, { color: "yellow" },
143
+ markedSessions.size > 0 && (React.createElement(Text, { color: "yellow" },
144
144
  " \u2022 ",
145
145
  markedSessions.size,
146
- " marked")),
146
+ " marked"))),
147
147
  React.createElement(Text, { color: "gray", dimColor: true }, "\u2191\u2193 navigate \u2022 Space mark \u2022 D delete \u2022 Enter select \u2022 ESC close")),
148
148
  hasPrevious && (React.createElement(Text, { color: "gray", dimColor: true },
149
- " \u2191 ",
149
+ ' ',
150
+ "\u2191 ",
150
151
  scrollOffset,
151
152
  " more above")),
152
153
  visibleSessions.map((session, index) => {
153
154
  const actualIndex = scrollOffset + index;
154
155
  const isSelected = actualIndex === selectedIndex;
155
156
  const isMarked = markedSessions.has(session.id);
156
- const title = session.title || 'Untitled';
157
+ // Remove newlines and other whitespace characters from title
158
+ const cleanTitle = (session.title || 'Untitled').replace(/[\r\n\t]+/g, ' ');
157
159
  const timeStr = formatDate(session.updatedAt);
158
- const truncatedLabel = title.length > 50 ? title.slice(0, 47) + '...' : title;
160
+ const truncatedLabel = cleanTitle.length > 50 ? cleanTitle.slice(0, 47) + '...' : cleanTitle;
159
161
  return (React.createElement(Box, { key: session.id },
160
162
  React.createElement(Text, { color: isMarked ? 'green' : 'gray' }, isMarked ? '✔ ' : ' '),
161
163
  React.createElement(Text, { color: isSelected ? 'green' : 'gray' }, isSelected ? '❯ ' : ' '),
162
164
  React.createElement(Text, { color: isSelected ? 'cyan' : isMarked ? 'green' : 'white' }, truncatedLabel),
163
165
  React.createElement(Text, { color: "gray", dimColor: true },
164
- " \u2022 ",
166
+ ' ',
167
+ "\u2022 ",
165
168
  timeStr)));
166
169
  }),
167
170
  hasMore && (React.createElement(Text, { color: "gray", dimColor: true },
168
- " \u2193 ",
171
+ ' ',
172
+ "\u2193 ",
169
173
  sessions.length - scrollOffset - VISIBLE_ITEMS,
170
174
  " more below"))));
171
175
  }
@@ -58,7 +58,8 @@ export default function SessionListScreen({ onBack, onSelectSession }) {
58
58
  const maxLabelWidth = Math.max(30, terminalWidth - reservedSpace);
59
59
  return sessions.map(session => {
60
60
  const timeString = formatDate(session.updatedAt);
61
- const title = session.title || 'Untitled';
61
+ // Remove newlines and other whitespace characters from title
62
+ const title = (session.title || 'Untitled').replace(/[\r\n\t]+/g, ' ');
62
63
  // Format: "Title • 5 msgs • 2h ago"
63
64
  const messageInfo = `${session.messageCount} msg${session.messageCount !== 1 ? 's' : ''}`;
64
65
  const fullLabel = `${title} • ${messageInfo} • ${timeString}`;
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
+ import TodoTree from './TodoTree.js';
3
4
  /**
4
5
  * Display a compact preview of tool execution results
5
6
  * Shows a tree-like structure with limited content
@@ -36,6 +37,9 @@ export default function ToolResultPreview({ toolName, result, maxLines = 5, }) {
36
37
  else if (toolName.startsWith('ace-')) {
37
38
  return renderACEPreview(toolName, data, maxLines);
38
39
  }
40
+ else if (toolName.startsWith('todo-')) {
41
+ return renderTodoPreview(toolName, data, maxLines);
42
+ }
39
43
  else {
40
44
  // Generic preview for unknown tools
41
45
  return renderGenericPreview(data, maxLines);
@@ -56,7 +60,7 @@ function renderSubAgentPreview(data, _maxLines) {
56
60
  React.createElement(Text, { color: "gray", dimColor: true },
57
61
  "\u2514\u2500 Sub-agent completed (",
58
62
  lines.length,
59
- " ",
63
+ ' ',
60
64
  lines.length === 1 ? 'line' : 'lines',
61
65
  " output)")));
62
66
  }
@@ -141,7 +145,7 @@ function renderACEPreview(toolName, data, maxLines) {
141
145
  React.createElement(Text, { color: "gray", dimColor: true },
142
146
  "\u2514\u2500 Found ",
143
147
  symbols.length,
144
- " ",
148
+ ' ',
145
149
  symbols.length === 1 ? 'symbol' : 'symbols')));
146
150
  }
147
151
  // Handle ace-find-references results
@@ -156,7 +160,7 @@ function renderACEPreview(toolName, data, maxLines) {
156
160
  React.createElement(Text, { color: "gray", dimColor: true },
157
161
  "\u2514\u2500 Found ",
158
162
  references.length,
159
- " ",
163
+ ' ',
160
164
  references.length === 1 ? 'reference' : 'references')));
161
165
  }
162
166
  // Handle ace-find-definition result
@@ -188,7 +192,7 @@ function renderACEPreview(toolName, data, maxLines) {
188
192
  React.createElement(Text, { color: "gray", dimColor: true },
189
193
  "\u2514\u2500 Found ",
190
194
  symbols.length,
191
- " ",
195
+ ' ',
192
196
  symbols.length === 1 ? 'symbol' : 'symbols',
193
197
  " in file")));
194
198
  }
@@ -204,12 +208,12 @@ function renderACEPreview(toolName, data, maxLines) {
204
208
  React.createElement(Text, { color: "gray", dimColor: true },
205
209
  "\u251C\u2500 ",
206
210
  data.symbols?.length || 0,
207
- " ",
211
+ ' ',
208
212
  (data.symbols?.length || 0) === 1 ? 'symbol' : 'symbols'),
209
213
  React.createElement(Text, { color: "gray", dimColor: true },
210
214
  "\u2514\u2500 ",
211
215
  data.references?.length || 0,
212
- " ",
216
+ ' ',
213
217
  (data.references?.length || 0) === 1 ? 'reference' : 'references')));
214
218
  }
215
219
  // Generic ACE tool preview
@@ -278,3 +282,26 @@ function renderGenericPreview(data, maxLines) {
278
282
  valueStr));
279
283
  })));
280
284
  }
285
+ function renderTodoPreview(_toolName, data, _maxLines) {
286
+ // Handle todo-create, todo-get, todo-update, todo-add, todo-delete
287
+ // Debug: Check if data is actually the stringified result that needs parsing again
288
+ // Some tools might return the result wrapped in content[0].text
289
+ let todoData = data;
290
+ // If data has content array (MCP format), extract the text
291
+ if (data.content && Array.isArray(data.content) && data.content[0]?.text) {
292
+ try {
293
+ todoData = JSON.parse(data.content[0].text);
294
+ }
295
+ catch (e) {
296
+ // If parsing fails, just use original data
297
+ }
298
+ }
299
+ if (!todoData.todos) {
300
+ return (React.createElement(Box, { marginLeft: 2 },
301
+ React.createElement(Text, { color: "gray", dimColor: true },
302
+ "\u2514\u2500 ",
303
+ todoData.message || 'No TODO list')));
304
+ }
305
+ // Use the TodoTree component to display the TODO list
306
+ return React.createElement(TodoTree, { todos: todoData.todos });
307
+ }
@@ -157,7 +157,7 @@ export default function ChatScreen({ skipWelcome }) {
157
157
  return () => {
158
158
  clearTimeout(handler);
159
159
  };
160
- }, [terminalWidth, stdout]);
160
+ }, [terminalWidth]); // stdout 对象可能在每次渲染时变化,移除以避免循环
161
161
  // Reload messages from session when remountKey changes (to restore sub-agent messages)
162
162
  useEffect(() => {
163
163
  if (remountKey === 0)
@@ -361,18 +361,26 @@ export default function ChatScreen({ skipWelcome }) {
361
361
  return;
362
362
  }
363
363
  }
364
- // Find the corresponding user message in session to delete
365
- // We start from the end and count backwards
366
- let sessionUserMessageCount = 0;
364
+ // Special case: if rolling back to index 0 (first message), always delete entire session
365
+ // This handles the case where user interrupts the first conversation
367
366
  let sessionTruncateIndex = currentSession.messages.length;
368
- for (let i = currentSession.messages.length - 1; i >= 0; i--) {
369
- const msg = currentSession.messages[i];
370
- if (msg && msg.role === 'user') {
371
- sessionUserMessageCount++;
372
- if (sessionUserMessageCount === uiUserMessagesToDelete) {
373
- // We want to delete from this user message onwards
374
- sessionTruncateIndex = i;
375
- break;
367
+ if (selectedIndex === 0) {
368
+ // Rolling back to the very first message means deleting entire session
369
+ sessionTruncateIndex = 0;
370
+ }
371
+ else {
372
+ // Find the corresponding user message in session to delete
373
+ // We start from the end and count backwards
374
+ let sessionUserMessageCount = 0;
375
+ for (let i = currentSession.messages.length - 1; i >= 0; i--) {
376
+ const msg = currentSession.messages[i];
377
+ if (msg && msg.role === 'user') {
378
+ sessionUserMessageCount++;
379
+ if (sessionUserMessageCount === uiUserMessagesToDelete) {
380
+ // We want to delete from this user message onwards
381
+ sessionTruncateIndex = i;
382
+ break;
383
+ }
376
384
  }
377
385
  }
378
386
  }
@@ -51,6 +51,10 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
51
51
  const [anthropicBeta, setAnthropicBeta] = useState(false);
52
52
  const [thinkingEnabled, setThinkingEnabled] = useState(false);
53
53
  const [thinkingBudgetTokens, setThinkingBudgetTokens] = useState(10000);
54
+ const [geminiThinkingEnabled, setGeminiThinkingEnabled] = useState(false);
55
+ const [geminiThinkingBudget, setGeminiThinkingBudget] = useState(1024);
56
+ const [responsesReasoningEnabled, setResponsesReasoningEnabled] = useState(false);
57
+ const [responsesReasoningEffort, setResponsesReasoningEffort] = useState('high');
54
58
  // Model settings
55
59
  const [advancedModel, setAdvancedModel] = useState('');
56
60
  const [basicModel, setBasicModel] = useState('');
@@ -101,7 +105,17 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
101
105
  'thinkingEnabled',
102
106
  'thinkingBudgetTokens',
103
107
  ]
104
- : []),
108
+ : requestMethod === 'gemini'
109
+ ? [
110
+ 'geminiThinkingEnabled',
111
+ 'geminiThinkingBudget',
112
+ ]
113
+ : requestMethod === 'responses'
114
+ ? [
115
+ 'responsesReasoningEnabled',
116
+ 'responsesReasoningEffort',
117
+ ]
118
+ : []),
105
119
  'advancedModel',
106
120
  'basicModel',
107
121
  'compactModelName',
@@ -126,6 +140,20 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
126
140
  currentField === 'thinkingBudgetTokens')) {
127
141
  setCurrentField('advancedModel');
128
142
  }
143
+ // If requestMethod is not 'gemini' and currentField is on Gemini-specific fields,
144
+ // move to the next available field
145
+ if (requestMethod !== 'gemini' &&
146
+ (currentField === 'geminiThinkingEnabled' ||
147
+ currentField === 'geminiThinkingBudget')) {
148
+ setCurrentField('advancedModel');
149
+ }
150
+ // If requestMethod is not 'responses' and currentField is on Responses-specific fields,
151
+ // move to the next available field
152
+ if (requestMethod !== 'responses' &&
153
+ (currentField === 'responsesReasoningEnabled' ||
154
+ currentField === 'responsesReasoningEffort')) {
155
+ setCurrentField('advancedModel');
156
+ }
129
157
  }, [requestMethod, currentField]);
130
158
  const loadProfilesAndConfig = () => {
131
159
  // Load profiles
@@ -139,6 +167,10 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
139
167
  setAnthropicBeta(config.anthropicBeta || false);
140
168
  setThinkingEnabled(config.thinking?.type === 'enabled' || false);
141
169
  setThinkingBudgetTokens(config.thinking?.budget_tokens || 10000);
170
+ setGeminiThinkingEnabled(config.geminiThinking?.enabled || false);
171
+ setGeminiThinkingBudget(config.geminiThinking?.budget || 1024);
172
+ setResponsesReasoningEnabled(config.responsesReasoning?.enabled || false);
173
+ setResponsesReasoningEffort(config.responsesReasoning?.effort || 'high');
142
174
  setAdvancedModel(config.advancedModel || '');
143
175
  setBasicModel(config.basicModel || '');
144
176
  setMaxContextTokens(config.maxContextTokens || 4000);
@@ -197,6 +229,10 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
197
229
  return maxTokens.toString();
198
230
  if (currentField === 'thinkingBudgetTokens')
199
231
  return thinkingBudgetTokens.toString();
232
+ if (currentField === 'geminiThinkingBudget')
233
+ return geminiThinkingBudget.toString();
234
+ if (currentField === 'responsesReasoningEffort')
235
+ return responsesReasoningEffort;
200
236
  if (currentField === 'compactModelName')
201
237
  return compactModelName;
202
238
  return '';
@@ -306,6 +342,26 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
306
342
  // Explicitly set to undefined to clear it when disabled
307
343
  config.thinking = undefined;
308
344
  }
345
+ // Save Gemini thinking configuration
346
+ if (geminiThinkingEnabled) {
347
+ config.geminiThinking = {
348
+ enabled: true,
349
+ budget: geminiThinkingBudget,
350
+ };
351
+ }
352
+ else {
353
+ config.geminiThinking = undefined;
354
+ }
355
+ // Save Responses reasoning configuration
356
+ if (responsesReasoningEnabled) {
357
+ config.responsesReasoning = {
358
+ enabled: true,
359
+ effort: responsesReasoningEffort,
360
+ };
361
+ }
362
+ else {
363
+ config.responsesReasoning = undefined;
364
+ }
309
365
  // Only save compactModel if modelName is provided (uses same baseUrl/apiKey)
310
366
  if (compactModelName) {
311
367
  config.compactModel = {
@@ -325,6 +381,12 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
325
381
  thinking: thinkingEnabled
326
382
  ? { type: 'enabled', budget_tokens: thinkingBudgetTokens }
327
383
  : undefined,
384
+ geminiThinking: geminiThinkingEnabled
385
+ ? { enabled: true, budget: geminiThinkingBudget }
386
+ : undefined,
387
+ responsesReasoning: responsesReasoningEnabled
388
+ ? { enabled: true, effort: responsesReasoningEffort }
389
+ : undefined,
328
390
  advancedModel,
329
391
  basicModel,
330
392
  maxContextTokens,
@@ -415,6 +477,51 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
415
477
  thinkingBudgetTokens))),
416
478
  !isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
417
479
  React.createElement(Text, { color: "gray" }, thinkingBudgetTokens)))));
480
+ case 'geminiThinkingEnabled':
481
+ return (React.createElement(Box, { key: field, flexDirection: "column" },
482
+ React.createElement(Text, { color: isActive ? 'green' : 'white' },
483
+ isActive ? '❯ ' : ' ',
484
+ "Gemini Thinking Enabled:"),
485
+ React.createElement(Box, { marginLeft: 3 },
486
+ React.createElement(Text, { color: "gray" },
487
+ geminiThinkingEnabled ? '☒ Enabled' : '☐ Disabled',
488
+ " (Press Enter to toggle)"))));
489
+ case 'geminiThinkingBudget':
490
+ return (React.createElement(Box, { key: field, flexDirection: "column" },
491
+ React.createElement(Text, { color: isActive ? 'green' : 'white' },
492
+ isActive ? '❯ ' : ' ',
493
+ "Gemini Thinking Budget:"),
494
+ isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
495
+ React.createElement(Text, { color: "cyan" },
496
+ "Enter value: ",
497
+ geminiThinkingBudget))),
498
+ !isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
499
+ React.createElement(Text, { color: "gray" }, geminiThinkingBudget)))));
500
+ case 'responsesReasoningEnabled':
501
+ return (React.createElement(Box, { key: field, flexDirection: "column" },
502
+ React.createElement(Text, { color: isActive ? 'green' : 'white' },
503
+ isActive ? '❯ ' : ' ',
504
+ "Responses Reasoning Enabled:"),
505
+ React.createElement(Box, { marginLeft: 3 },
506
+ React.createElement(Text, { color: "gray" },
507
+ responsesReasoningEnabled ? '☒ Enabled' : '☐ Disabled',
508
+ " (Press Enter to toggle)"))));
509
+ case 'responsesReasoningEffort':
510
+ return (React.createElement(Box, { key: field, flexDirection: "column" },
511
+ React.createElement(Text, { color: isActive ? 'green' : 'white' },
512
+ isActive ? '❯ ' : ' ',
513
+ "Responses Reasoning Effort:"),
514
+ !isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
515
+ React.createElement(Text, { color: "gray" }, responsesReasoningEffort.toUpperCase()))),
516
+ isCurrentlyEditing && (React.createElement(Box, { marginLeft: 3 },
517
+ React.createElement(Select, { options: [
518
+ { label: 'Low', value: 'low' },
519
+ { label: 'Medium', value: 'medium' },
520
+ { label: 'High', value: 'high' },
521
+ ], defaultValue: responsesReasoningEffort, onChange: value => {
522
+ setResponsesReasoningEffort(value);
523
+ setIsEditing(false);
524
+ } })))));
418
525
  case 'advancedModel':
419
526
  return (React.createElement(Box, { key: field, flexDirection: "column" },
420
527
  React.createElement(Text, { color: isActive ? 'green' : 'white' },
@@ -561,7 +668,8 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
561
668
  currentField === 'requestMethod' ||
562
669
  currentField === 'advancedModel' ||
563
670
  currentField === 'basicModel' ||
564
- currentField === 'compactModelName') &&
671
+ currentField === 'compactModelName' ||
672
+ currentField === 'responsesReasoningEffort') &&
565
673
  key.escape) {
566
674
  setIsEditing(false);
567
675
  setSearchTerm('');
@@ -581,13 +689,16 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
581
689
  // Handle numeric input for token fields
582
690
  if (currentField === 'maxContextTokens' ||
583
691
  currentField === 'maxTokens' ||
584
- currentField === 'thinkingBudgetTokens') {
692
+ currentField === 'thinkingBudgetTokens' ||
693
+ currentField === 'geminiThinkingBudget') {
585
694
  if (input && input.match(/[0-9]/)) {
586
695
  const currentValue = currentField === 'maxContextTokens'
587
696
  ? maxContextTokens
588
697
  : currentField === 'maxTokens'
589
698
  ? maxTokens
590
- : thinkingBudgetTokens;
699
+ : currentField === 'thinkingBudgetTokens'
700
+ ? thinkingBudgetTokens
701
+ : geminiThinkingBudget;
591
702
  const newValue = parseInt(currentValue.toString() + input, 10);
592
703
  if (!isNaN(newValue)) {
593
704
  if (currentField === 'maxContextTokens') {
@@ -596,9 +707,12 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
596
707
  else if (currentField === 'maxTokens') {
597
708
  setMaxTokens(newValue);
598
709
  }
599
- else {
710
+ else if (currentField === 'thinkingBudgetTokens') {
600
711
  setThinkingBudgetTokens(newValue);
601
712
  }
713
+ else {
714
+ setGeminiThinkingBudget(newValue);
715
+ }
602
716
  }
603
717
  }
604
718
  else if (key.backspace || key.delete) {
@@ -606,7 +720,9 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
606
720
  ? maxContextTokens
607
721
  : currentField === 'maxTokens'
608
722
  ? maxTokens
609
- : thinkingBudgetTokens;
723
+ : currentField === 'thinkingBudgetTokens'
724
+ ? thinkingBudgetTokens
725
+ : geminiThinkingBudget;
610
726
  const currentStr = currentValue.toString();
611
727
  const newStr = currentStr.slice(0, -1);
612
728
  const newValue = parseInt(newStr, 10);
@@ -616,21 +732,28 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
616
732
  else if (currentField === 'maxTokens') {
617
733
  setMaxTokens(!isNaN(newValue) ? newValue : 0);
618
734
  }
619
- else {
735
+ else if (currentField === 'thinkingBudgetTokens') {
620
736
  setThinkingBudgetTokens(!isNaN(newValue) ? newValue : 0);
621
737
  }
738
+ else {
739
+ setGeminiThinkingBudget(!isNaN(newValue) ? newValue : 0);
740
+ }
622
741
  }
623
742
  else if (key.return) {
624
743
  const minValue = currentField === 'maxContextTokens'
625
744
  ? 4000
626
745
  : currentField === 'maxTokens'
627
746
  ? 100
628
- : 1000;
747
+ : currentField === 'thinkingBudgetTokens'
748
+ ? 1000
749
+ : 1;
629
750
  const currentValue = currentField === 'maxContextTokens'
630
751
  ? maxContextTokens
631
752
  : currentField === 'maxTokens'
632
753
  ? maxTokens
633
- : thinkingBudgetTokens;
754
+ : currentField === 'thinkingBudgetTokens'
755
+ ? thinkingBudgetTokens
756
+ : geminiThinkingBudget;
634
757
  const finalValue = currentValue < minValue ? minValue : currentValue;
635
758
  if (currentField === 'maxContextTokens') {
636
759
  setMaxContextTokens(finalValue);
@@ -638,9 +761,12 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
638
761
  else if (currentField === 'maxTokens') {
639
762
  setMaxTokens(finalValue);
640
763
  }
641
- else {
764
+ else if (currentField === 'thinkingBudgetTokens') {
642
765
  setThinkingBudgetTokens(finalValue);
643
766
  }
767
+ else {
768
+ setGeminiThinkingBudget(finalValue);
769
+ }
644
770
  setIsEditing(false);
645
771
  }
646
772
  return;
@@ -676,9 +802,19 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
676
802
  else if (currentField === 'thinkingEnabled') {
677
803
  setThinkingEnabled(!thinkingEnabled);
678
804
  }
805
+ else if (currentField === 'geminiThinkingEnabled') {
806
+ setGeminiThinkingEnabled(!geminiThinkingEnabled);
807
+ }
808
+ else if (currentField === 'responsesReasoningEnabled') {
809
+ setResponsesReasoningEnabled(!responsesReasoningEnabled);
810
+ }
679
811
  else if (currentField === 'maxContextTokens' ||
680
812
  currentField === 'maxTokens' ||
681
- currentField === 'thinkingBudgetTokens') {
813
+ currentField === 'thinkingBudgetTokens' ||
814
+ currentField === 'geminiThinkingBudget') {
815
+ setIsEditing(true);
816
+ }
817
+ else if (currentField === 'responsesReasoningEffort') {
682
818
  setIsEditing(true);
683
819
  }
684
820
  else if (currentField === 'advancedModel' ||
@@ -819,7 +955,8 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
819
955
  currentField === 'requestMethod' ||
820
956
  currentField === 'advancedModel' ||
821
957
  currentField === 'basicModel' ||
822
- currentField === 'compactModelName') ? (React.createElement(Box, { flexDirection: "column" },
958
+ currentField === 'compactModelName' ||
959
+ currentField === 'responsesReasoningEffort') ? (React.createElement(Box, { flexDirection: "column" },
823
960
  React.createElement(Text, { color: "green" },
824
961
  "\u276F ",
825
962
  currentField === 'profile' && 'Profile',
@@ -827,6 +964,7 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
827
964
  currentField === 'advancedModel' && 'Advanced Model',
828
965
  currentField === 'basicModel' && 'Basic Model',
829
966
  currentField === 'compactModelName' && 'Compact Model',
967
+ currentField === 'responsesReasoningEffort' && 'Responses Reasoning Effort',
830
968
  ":"),
831
969
  React.createElement(Box, { marginLeft: 3, marginTop: 1 },
832
970
  currentField === 'profile' && (React.createElement(Box, { flexDirection: "column" },
@@ -857,13 +995,23 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
857
995
  searchTerm && React.createElement(Text, { color: "cyan" },
858
996
  "Filter: ",
859
997
  searchTerm),
860
- React.createElement(Select, { options: getCurrentOptions(), defaultValue: getCurrentValue(), onChange: handleModelChange })))),
998
+ React.createElement(Select, { options: getCurrentOptions(), defaultValue: getCurrentValue(), onChange: handleModelChange }))),
999
+ currentField === 'responsesReasoningEffort' && (React.createElement(Select, { options: [
1000
+ { label: 'Low', value: 'low' },
1001
+ { label: 'Medium', value: 'medium' },
1002
+ { label: 'High', value: 'high' },
1003
+ ], defaultValue: responsesReasoningEffort, onChange: value => {
1004
+ setResponsesReasoningEffort(value);
1005
+ setIsEditing(false);
1006
+ } }))),
861
1007
  React.createElement(Box, { marginTop: 1 },
862
1008
  React.createElement(Alert, { variant: "info" },
863
1009
  (currentField === 'advancedModel' ||
864
1010
  currentField === 'basicModel' ||
865
1011
  currentField === 'compactModelName') &&
866
1012
  'Type to filter, ↑↓ to select, Enter to confirm, Esc to cancel',
1013
+ currentField === 'responsesReasoningEffort' &&
1014
+ '↑↓ to select, Enter to confirm, Esc to cancel',
867
1015
  currentField === 'profile' &&
868
1016
  '↑↓ to select profile, N to create new, D to delete, Enter to confirm, Esc to cancel',
869
1017
  currentField === 'requestMethod' &&
@@ -894,7 +1042,8 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
894
1042
  currentField === 'requestMethod' ||
895
1043
  currentField === 'advancedModel' ||
896
1044
  currentField === 'basicModel' ||
897
- currentField === 'compactModelName')) && (React.createElement(Box, { flexDirection: "column", marginTop: 1 }, isEditing ? (React.createElement(Alert, { variant: "info" },
1045
+ currentField === 'compactModelName' ||
1046
+ currentField === 'responsesReasoningEffort')) && (React.createElement(Box, { flexDirection: "column", marginTop: 1 }, isEditing ? (React.createElement(Alert, { variant: "info" },
898
1047
  "Editing mode:",
899
1048
  ' ',
900
1049
  currentField === 'maxContextTokens' ||
@@ -6,6 +6,14 @@ export interface ThinkingConfig {
6
6
  type: 'enabled';
7
7
  budget_tokens: number;
8
8
  }
9
+ export interface GeminiThinkingConfig {
10
+ enabled: boolean;
11
+ budget: number;
12
+ }
13
+ export interface ResponsesReasoningConfig {
14
+ enabled: boolean;
15
+ effort: 'low' | 'medium' | 'high';
16
+ }
9
17
  export interface ApiConfig {
10
18
  baseUrl: string;
11
19
  apiKey: string;
@@ -17,6 +25,8 @@ export interface ApiConfig {
17
25
  compactModel?: CompactModelConfig;
18
26
  anthropicBeta?: boolean;
19
27
  thinking?: ThinkingConfig;
28
+ geminiThinking?: GeminiThinkingConfig;
29
+ responsesReasoning?: ResponsesReasoningConfig;
20
30
  }
21
31
  export interface MCPServer {
22
32
  url?: string;
@@ -1,4 +1,5 @@
1
1
  import { formatToolCallMessage } from './messageFormatter.js';
2
+ import { isToolNeedTwoStepDisplay } from './toolDisplayConfig.js';
2
3
  /**
3
4
  * Convert API format session messages to UI format messages
4
5
  * Process messages in order to maintain correct sequence
@@ -114,17 +115,21 @@ export function convertSessionMessagesToUI(sessionMessages) {
114
115
  catch (e) {
115
116
  toolArgs = {};
116
117
  }
117
- // Add tool call message
118
- uiMessages.push({
119
- role: 'assistant',
120
- content: `⚡ ${toolDisplay.toolName}`,
121
- streaming: false,
122
- toolCall: {
123
- name: toolCall.function.name,
124
- arguments: toolArgs,
125
- },
126
- toolDisplay,
127
- });
118
+ // Only add "in progress" message for tools that need two-step display
119
+ const needTwoSteps = isToolNeedTwoStepDisplay(toolCall.function.name);
120
+ if (needTwoSteps) {
121
+ // Add tool call message (in progress)
122
+ uiMessages.push({
123
+ role: 'assistant',
124
+ content: `⚡ ${toolDisplay.toolName}`,
125
+ streaming: false,
126
+ toolCall: {
127
+ name: toolCall.function.name,
128
+ arguments: toolArgs,
129
+ },
130
+ toolDisplay,
131
+ });
132
+ }
128
133
  processedToolCalls.add(toolCall.id);
129
134
  }
130
135
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.3.22",
3
+ "version": "0.3.23",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {