snow-ai 0.3.20 → 0.3.22

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.
@@ -369,7 +369,10 @@ export class FilesystemMCPService {
369
369
  });
370
370
  }
371
371
  // Single file mode
372
- if (!searchContent || !replaceContent) {
372
+ if (searchContent === undefined ||
373
+ searchContent === null ||
374
+ replaceContent === undefined ||
375
+ replaceContent === null) {
373
376
  throw new Error('searchContent and replaceContent are required for single file mode');
374
377
  }
375
378
  return await this.editFileBySearchSingle(filePath, searchContent, replaceContent, occurrence, contextLines);
@@ -201,12 +201,20 @@ function formatTokens(tokens, compact = false) {
201
201
  }
202
202
  return String(tokens);
203
203
  }
204
- function renderStackedBarChart(stats, terminalWidth) {
204
+ function renderStackedBarChart(stats, terminalWidth, scrollOffset) {
205
205
  if (stats.models.size === 0) {
206
206
  return (React.createElement(Text, { color: "gray", dimColor: true }, "No data available"));
207
207
  }
208
208
  const sortedModels = Array.from(stats.models.entries()).sort((a, b) => b[1].total - a[1].total);
209
209
  const isNarrow = terminalWidth < 100;
210
+ // Show maximum 2 models at a time for better readability
211
+ const maxVisibleModels = 2;
212
+ // Calculate visible range
213
+ const startIdx = scrollOffset;
214
+ const endIdx = Math.min(startIdx + maxVisibleModels, sortedModels.length);
215
+ const visibleModels = sortedModels.slice(startIdx, endIdx);
216
+ const hasMoreAbove = startIdx > 0;
217
+ const hasMoreBelow = endIdx < sortedModels.length;
210
218
  // Calculate max total (including cache) for scaling
211
219
  const maxTotal = Math.max(...Array.from(stats.models.values()).map(s => s.total + s.cacheCreation + s.cacheRead));
212
220
  // Use almost full width for bars (leave some margin)
@@ -227,7 +235,12 @@ function renderStackedBarChart(stats, terminalWidth) {
227
235
  React.createElement(Text, { color: "gray", dimColor: true },
228
236
  ' ',
229
237
  "Cache Create")),
230
- sortedModels.map(([modelName, modelStats]) => {
238
+ hasMoreAbove && (React.createElement(Box, { marginBottom: 1 },
239
+ React.createElement(Text, { color: "yellow", dimColor: true },
240
+ "\u2191 ",
241
+ startIdx,
242
+ " more above (use \u2191 arrow)"))),
243
+ visibleModels.map(([modelName, modelStats]) => {
231
244
  const shortName = getModelShortName(modelName, 30);
232
245
  // Calculate segment lengths based on proportion
233
246
  // Ensure at least 1 character if value exists
@@ -298,7 +311,12 @@ function renderStackedBarChart(stats, terminalWidth) {
298
311
  React.createElement(Text, { color: "yellow", bold: true },
299
312
  "Create:",
300
313
  ' ',
301
- formatTokens(Array.from(stats.models.values()).reduce((sum, s) => sum + s.cacheCreation, 0))))))))));
314
+ formatTokens(Array.from(stats.models.values()).reduce((sum, s) => sum + s.cacheCreation, 0)))))))),
315
+ hasMoreBelow && (React.createElement(Box, { marginTop: 1 },
316
+ React.createElement(Text, { color: "yellow", dimColor: true },
317
+ "\u2193 ",
318
+ sortedModels.length - endIdx,
319
+ " more below (use \u2193 arrow)")))));
302
320
  }
303
321
  export default function UsagePanel() {
304
322
  const [granularity, setGranularity] = useState('week');
@@ -308,6 +326,7 @@ export default function UsagePanel() {
308
326
  });
309
327
  const [isLoading, setIsLoading] = useState(true);
310
328
  const [error, setError] = useState(null);
329
+ const [scrollOffset, setScrollOffset] = useState(0);
311
330
  const { columns: terminalWidth } = useTerminalSize();
312
331
  useEffect(() => {
313
332
  const load = async () => {
@@ -328,6 +347,10 @@ export default function UsagePanel() {
328
347
  };
329
348
  load();
330
349
  }, [granularity]);
350
+ // Reset scroll when changing granularity
351
+ useEffect(() => {
352
+ setScrollOffset(0);
353
+ }, [granularity]);
331
354
  useInput((_input, key) => {
332
355
  if (key.tab) {
333
356
  const granularities = ['hour', 'day', 'week', 'month'];
@@ -335,6 +358,17 @@ export default function UsagePanel() {
335
358
  const nextIdx = (currentIdx + 1) % granularities.length;
336
359
  setGranularity(granularities[nextIdx]);
337
360
  }
361
+ // Calculate available space for scrolling
362
+ const sortedModels = Array.from(stats.models.entries()).sort((a, b) => b[1].total - a[1].total);
363
+ const totalModels = sortedModels.length;
364
+ if (key.upArrow) {
365
+ setScrollOffset(prev => Math.max(0, prev - 1));
366
+ }
367
+ if (key.downArrow) {
368
+ // Reserve space for header, legend, total summary
369
+ const maxScroll = Math.max(0, totalModels - 1);
370
+ setScrollOffset(prev => Math.min(maxScroll, prev + 1));
371
+ }
338
372
  });
339
373
  if (isLoading) {
340
374
  return (React.createElement(Box, { borderColor: "cyan", borderStyle: "round", paddingX: 2, paddingY: 0 },
@@ -356,5 +390,5 @@ export default function UsagePanel() {
356
390
  React.createElement(Text, { color: "gray", dimColor: true },
357
391
  ' ',
358
392
  "- Tab to switch")),
359
- stats.models.size === 0 ? (React.createElement(Text, { color: "gray", dimColor: true }, "No usage data for this period")) : (renderStackedBarChart(stats, terminalWidth))));
393
+ stats.models.size === 0 ? (React.createElement(Text, { color: "gray", dimColor: true }, "No usage data for this period")) : (renderStackedBarChart(stats, terminalWidth, scrollOffset))));
360
394
  }
@@ -323,9 +323,20 @@ export default function ChatScreen({ skipWelcome }) {
323
323
  // We need to truncate to the same user message in the session file
324
324
  if (currentSession) {
325
325
  // Count how many user messages we're deleting (from selectedIndex onwards in UI)
326
- const uiUserMessagesToDelete = messages
327
- .slice(selectedIndex)
328
- .filter(msg => msg.role === 'user').length;
326
+ // But exclude any uncommitted user messages that weren't saved to session
327
+ const messagesAfterSelected = messages.slice(selectedIndex);
328
+ const hasDiscontinuedMessage = messagesAfterSelected.some(msg => msg.discontinued);
329
+ let uiUserMessagesToDelete = 0;
330
+ if (hasDiscontinuedMessage) {
331
+ // If there's a discontinued message, it means all messages from selectedIndex onwards
332
+ // (including user messages) were not saved to session
333
+ // So we don't need to delete any user messages from session
334
+ uiUserMessagesToDelete = 0;
335
+ }
336
+ else {
337
+ // Normal case: count all user messages from selectedIndex onwards
338
+ uiUserMessagesToDelete = messagesAfterSelected.filter(msg => msg.role === 'user').length;
339
+ }
329
340
  // Check if the selected message is a user message that might not be in session
330
341
  // (e.g., interrupted before AI response)
331
342
  const selectedMessage = messages[selectedIndex];
@@ -621,9 +632,25 @@ export default function ChatScreen({ skipWelcome }) {
621
632
  }
622
633
  }
623
634
  // If some tool results are missing, remove from this assistant message onwards
635
+ // But only if this is the last assistant message with tool_calls in the entire conversation
624
636
  if (toolCallIds.size > 0) {
625
- truncateIndex = i;
626
- break;
637
+ // Additional check: ensure this is the last assistant message with tool_calls
638
+ let hasLaterAssistantWithTools = false;
639
+ for (let k = i + 1; k < messages.length; k++) {
640
+ const laterMsg = messages[k];
641
+ if (laterMsg?.role === 'assistant' &&
642
+ laterMsg?.tool_calls &&
643
+ laterMsg.tool_calls.length > 0) {
644
+ hasLaterAssistantWithTools = true;
645
+ break;
646
+ }
647
+ }
648
+ // Only truncate if no later assistant messages have tool_calls
649
+ // This preserves complete historical conversations
650
+ if (!hasLaterAssistantWithTools) {
651
+ truncateIndex = i;
652
+ break;
653
+ }
627
654
  }
628
655
  }
629
656
  // If we found a complete assistant response without tool calls, we're done
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {