tabby-ai-assistant 1.0.13 → 1.0.15

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 (41) hide show
  1. package/.editorconfig +18 -0
  2. package/dist/index.js +1 -1
  3. package/package.json +6 -4
  4. package/src/components/chat/ai-sidebar.component.scss +220 -9
  5. package/src/components/chat/ai-sidebar.component.ts +364 -29
  6. package/src/components/chat/chat-input.component.ts +36 -4
  7. package/src/components/chat/chat-interface.component.ts +225 -5
  8. package/src/components/chat/chat-message.component.ts +6 -1
  9. package/src/components/settings/context-settings.component.ts +91 -91
  10. package/src/components/terminal/ai-toolbar-button.component.ts +4 -2
  11. package/src/components/terminal/command-suggestion.component.ts +148 -6
  12. package/src/index.ts +0 -6
  13. package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
  14. package/src/services/chat/ai-sidebar.service.ts +414 -410
  15. package/src/services/chat/chat-session.service.ts +36 -12
  16. package/src/services/context/compaction.ts +110 -134
  17. package/src/services/context/manager.ts +27 -7
  18. package/src/services/context/memory.ts +17 -33
  19. package/src/services/context/summary.service.ts +136 -0
  20. package/src/services/core/ai-assistant.service.ts +1060 -37
  21. package/src/services/core/ai-provider-manager.service.ts +154 -25
  22. package/src/services/core/checkpoint.service.ts +218 -18
  23. package/src/services/core/toast.service.ts +106 -106
  24. package/src/services/providers/anthropic-provider.service.ts +126 -30
  25. package/src/services/providers/base-provider.service.ts +90 -7
  26. package/src/services/providers/glm-provider.service.ts +151 -38
  27. package/src/services/providers/minimax-provider.service.ts +55 -40
  28. package/src/services/providers/ollama-provider.service.ts +117 -28
  29. package/src/services/providers/openai-compatible.service.ts +164 -34
  30. package/src/services/providers/openai-provider.service.ts +169 -34
  31. package/src/services/providers/vllm-provider.service.ts +116 -28
  32. package/src/services/terminal/terminal-context.service.ts +265 -5
  33. package/src/services/terminal/terminal-manager.service.ts +748 -748
  34. package/src/services/terminal/terminal-tools.service.ts +612 -441
  35. package/src/types/ai.types.ts +156 -3
  36. package/src/utils/cost.utils.ts +249 -0
  37. package/src/utils/validation.utils.ts +306 -2
  38. package/dist/index.js.LICENSE.txt +0 -18
  39. package/src/services/terminal/command-analyzer.service.ts +0 -43
  40. package/src/services/terminal/context-menu.service.ts +0 -45
  41. package/src/services/terminal/hotkey.service.ts +0 -53
@@ -1,7 +1,7 @@
1
1
  import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked, AfterViewInit, ViewEncapsulation, HostBinding } from '@angular/core';
2
2
  import { Subject } from 'rxjs';
3
3
  import { takeUntil } from 'rxjs/operators';
4
- import { ChatMessage, MessageRole } from '../../types/ai.types';
4
+ import { ChatMessage, MessageRole, StreamEvent, AgentStreamEvent } from '../../types/ai.types';
5
5
  import { AiAssistantService } from '../../services/core/ai-assistant.service';
6
6
  import { ConfigProviderService } from '../../services/core/config-provider.service';
7
7
  import { LoggerService } from '../../services/core/logger.service';
@@ -325,9 +325,10 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
325
325
  }
326
326
 
327
327
  /**
328
- * 处理发送消息
328
+ * 处理发送消息 - 使用 Agent 循环模式
329
+ * 支持多轮工具调用自动循环
329
330
  */
330
- async onSendMessage(content: string): Promise<void> {
331
+ async onSendMessageWithAgent(content: string): Promise<void> {
331
332
  if (!content.trim() || this.isLoading) {
332
333
  return;
333
334
  }
@@ -344,42 +345,343 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
344
345
  // 滚动到底部
345
346
  setTimeout(() => this.scrollToBottom(), 0);
346
347
 
347
- // 清空输入框
348
- content = '';
349
-
350
348
  // 显示加载状态
351
349
  this.isLoading = true;
352
350
 
351
+ // 创建 AI 消息占位符用于流式更新
352
+ const aiMessage: ChatMessage = {
353
+ id: this.generateId(),
354
+ role: MessageRole.ASSISTANT,
355
+ content: '',
356
+ timestamp: new Date()
357
+ };
358
+ this.messages.push(aiMessage);
359
+
360
+ // 工具调用跟踪
361
+ const toolStatus = new Map<string, { name: string; startTime: number }>();
362
+
353
363
  try {
354
- // 发送请求到AI
355
- const response = await this.aiService.chat({
356
- messages: this.messages,
357
- maxTokens: 1000,
364
+ // 使用 Agent 循环流式 API
365
+ this.aiService.chatStreamWithAgentLoop({
366
+ messages: this.messages.slice(0, -1),
367
+ maxTokens: 2000,
358
368
  temperature: 0.7
369
+ }, {
370
+ maxRounds: 5
371
+ }).pipe(
372
+ takeUntil(this.destroy$)
373
+ ).subscribe({
374
+ next: (event: AgentStreamEvent) => {
375
+ switch (event.type) {
376
+ case 'text_delta':
377
+ // 文本流式显示
378
+ if (event.textDelta) {
379
+ aiMessage.content += event.textDelta;
380
+ this.shouldScrollToBottom = true;
381
+ }
382
+ break;
383
+
384
+ case 'tool_use_start':
385
+ // 工具开始 - 使用专门的工具卡片样式
386
+ const toolName = event.toolCall?.name || 'unknown';
387
+ aiMessage.content += `
388
+ <div class="tool-call-card tool-executing">
389
+ <div class="tool-header">
390
+ <span class="tool-icon">🔧</span>
391
+ <span class="tool-name">${toolName}</span>
392
+ <span class="tool-status" id="tool-status-${event.toolCall?.id}">执行中...</span>
393
+ </div>
394
+ </div>`;
395
+ if (event.toolCall?.id) {
396
+ toolStatus.set(event.toolCall.id, {
397
+ name: toolName,
398
+ startTime: Date.now()
399
+ });
400
+ }
401
+ this.shouldScrollToBottom = true;
402
+ break;
403
+
404
+ case 'tool_executing':
405
+ // 工具正在执行(额外状态)
406
+ break;
407
+
408
+ case 'tool_executed':
409
+ // 工具完成 - 更新工具卡片
410
+ if (event.toolCall && event.toolResult) {
411
+ const name = toolStatus.get(event.toolCall.id)?.name || event.toolCall.name || 'unknown';
412
+ const duration = event.toolResult.duration || 0;
413
+
414
+ // 更新工具卡片状态
415
+ const executingCardRegex = new RegExp(
416
+ `<div class="tool-call-card tool-executing">\\s*<div class="tool-header">\\s*<span class="tool-icon">🔧</span>\\s*<span class="tool-name">${name}</span>[^]*?</div>\\s*</div>`,
417
+ 'g'
418
+ );
419
+
420
+ // 构建工具结果卡片
421
+ let toolCard = `
422
+ <div class="tool-call-card tool-success">
423
+ <div class="tool-header">
424
+ <span class="tool-icon">✅</span>
425
+ <span class="tool-name">${name}</span>
426
+ <span class="tool-duration">${duration}ms</span>
427
+ </div>`;
428
+
429
+ // 添加工具输出
430
+ if (event.toolResult.content && !event.toolResult.is_error) {
431
+ const preview = event.toolResult.content.substring(0, 500);
432
+ const truncated = event.toolResult.content.length > 500 ? '...' : '';
433
+ toolCard += `
434
+ <div class="tool-output">
435
+ <div class="tool-output-header">输出:</div>
436
+ <pre class="tool-output-content">${this.escapeHtml(preview)}${truncated}</pre>
437
+ </div>`;
438
+ }
439
+
440
+ toolCard += `</div>`;
441
+
442
+ aiMessage.content = aiMessage.content.replace(executingCardRegex, toolCard);
443
+ toolStatus.delete(event.toolCall.id);
444
+ }
445
+ this.shouldScrollToBottom = true;
446
+ break;
447
+
448
+ case 'tool_error':
449
+ // 工具错误 - 更新工具卡片为错误状态
450
+ if (event.toolCall) {
451
+ const name = toolStatus.get(event.toolCall.id)?.name || event.toolCall.name || 'unknown';
452
+ const errorMsg = event.toolResult?.content || 'Unknown error';
453
+
454
+ const executingCardRegex = new RegExp(
455
+ `<div class="tool-call-card tool-executing">\\s*<div class="tool-header">\\s*<span class="tool-icon">🔧</span>\\s*<span class="tool-name">${name}</span>[^]*?</div>\\s*</div>`,
456
+ 'g'
457
+ );
458
+
459
+ const errorCard = `
460
+ <div class="tool-call-card tool-error">
461
+ <div class="tool-header">
462
+ <span class="tool-icon">❌</span>
463
+ <span class="tool-name">${name}</span>
464
+ <span class="tool-status">失败</span>
465
+ </div>
466
+ <div class="tool-output tool-error-message">
467
+ <pre class="tool-output-content">${this.escapeHtml(errorMsg)}</pre>
468
+ </div>
469
+ </div>`;
470
+
471
+ aiMessage.content = aiMessage.content.replace(executingCardRegex, errorCard);
472
+ toolStatus.delete(event.toolCall.id);
473
+ }
474
+ this.shouldScrollToBottom = true;
475
+ break;
476
+
477
+ case 'round_start':
478
+ // 新一轮开始
479
+ if (event.round && event.round > 1) {
480
+ aiMessage.content += '\n\n---\n\n';
481
+ }
482
+ break;
483
+
484
+ case 'round_end':
485
+ // 一轮结束
486
+ break;
487
+
488
+ case 'agent_complete':
489
+ // Agent 循环完成 - 显示终止原因
490
+ this.logger.info('Agent completed', {
491
+ reason: event.reason,
492
+ totalRounds: event.totalRounds
493
+ });
494
+
495
+ // 终止原因映射
496
+ const reasonText: Record<string, { icon: string; label: string }> = {
497
+ 'task_complete': { icon: '✅', label: '任务完成' },
498
+ 'no_tools': { icon: '✅', label: '已执行完成' },
499
+ 'summarizing': { icon: '✅', label: '总结完成' },
500
+ 'repeated_tool': { icon: '⚠️', label: '检测到重复操作' },
501
+ 'high_failure_rate': { icon: '⚠️', label: '多次调用失败' },
502
+ 'timeout': { icon: '⏱️', label: '执行超时' },
503
+ 'max_rounds': { icon: '⚠️', label: '达到最大轮数' },
504
+ 'user_cancel': { icon: '🛑', label: '用户取消' }
505
+ };
506
+
507
+ const reasonInfo = reasonText[event.reason || ''] || { icon: '📌', label: '完成' };
508
+ const roundsText = event.totalRounds ? ` (${event.totalRounds} 轮)` : '';
509
+
510
+ // 添加终止信息
511
+ aiMessage.content += `\n\n---\n**${reasonInfo.icon} ${reasonInfo.label}**${roundsText}`;
512
+
513
+ // 如果有终止消息,添加到内容中
514
+ if (event.terminationMessage) {
515
+ aiMessage.content += `\n${event.terminationMessage}`;
516
+ }
517
+
518
+ this.shouldScrollToBottom = true;
519
+ break;
520
+
521
+ case 'error':
522
+ // 错误
523
+ aiMessage.content += `\n\n❌ 错误: ${event.error}`;
524
+ this.shouldScrollToBottom = true;
525
+ break;
526
+ }
527
+ },
528
+ error: (error) => {
529
+ this.logger.error('Agent stream error', error);
530
+ aiMessage.content += `\n\n❌ 错误: ${error instanceof Error ? error.message : 'Unknown error'}`;
531
+ this.isLoading = false;
532
+ this.shouldScrollToBottom = true;
533
+ this.saveChatHistory();
534
+ },
535
+ complete: () => {
536
+ this.isLoading = false;
537
+ this.updateTokenUsage();
538
+ this.saveChatHistory();
539
+ this.shouldScrollToBottom = true;
540
+ }
359
541
  });
360
542
 
361
- // 添加AI响应
362
- this.messages.push(response.message);
543
+ } catch (error) {
544
+ this.logger.error('Failed to send message with agent', error);
545
+ aiMessage.content = `抱歉,我遇到了一些问题:${error instanceof Error ? error.message : 'Unknown error'}\n\n请稍后重试。`;
546
+ this.isLoading = false;
547
+ this.updateTokenUsage();
548
+ setTimeout(() => this.scrollToBottom(), 0);
549
+ }
550
+ }
551
+
552
+ /**
553
+ * 处理发送消息 - 原有方法(保留兼容性)
554
+ */
555
+ async onSendMessage(content: string): Promise<void> {
556
+ if (!content.trim() || this.isLoading) {
557
+ return;
558
+ }
559
+
560
+ // 添加用户消息
561
+ const userMessage: ChatMessage = {
562
+ id: this.generateId(),
563
+ role: MessageRole.USER,
564
+ content: content.trim(),
565
+ timestamp: new Date()
566
+ };
567
+ this.messages.push(userMessage);
568
+
569
+ // 滚动到底部
570
+ setTimeout(() => this.scrollToBottom(), 0);
571
+
572
+ // 显示加载状态
573
+ this.isLoading = true;
574
+
575
+ // 创建 AI 消息占位符用于流式更新
576
+ const aiMessage: ChatMessage = {
577
+ id: this.generateId(),
578
+ role: MessageRole.ASSISTANT,
579
+ content: '',
580
+ timestamp: new Date()
581
+ };
582
+ this.messages.push(aiMessage);
363
583
 
364
- // 保存聊天历史
365
- this.saveChatHistory();
584
+ // 工具调用跟踪
585
+ const pendingTools = new Map<string, { name: string; startTime: number }>();
586
+ const toolResults: string[] = [];
587
+
588
+ try {
589
+ // 使用流式 API
590
+ this.aiService.chatStream({
591
+ messages: this.messages.slice(0, -1), // 排除刚添加的空 AI 消息
592
+ maxTokens: 2000,
593
+ temperature: 0.7
594
+ }).pipe(
595
+ takeUntil(this.destroy$)
596
+ ).subscribe({
597
+ next: (event: StreamEvent) => {
598
+ switch (event.type) {
599
+ case 'text_delta':
600
+ // 文本流式显示
601
+ if (event.textDelta) {
602
+ aiMessage.content += event.textDelta;
603
+ this.shouldScrollToBottom = true;
604
+ }
605
+ break;
606
+
607
+ case 'tool_use_start':
608
+ // 工具开始 - 显示工具名称
609
+ const toolName = event.toolCall?.name || 'unknown';
610
+ aiMessage.content += `\n\n🔧 正在执行 ${toolName}...`;
611
+ if (event.toolCall?.id) {
612
+ pendingTools.set(event.toolCall.id, {
613
+ name: toolName,
614
+ startTime: Date.now()
615
+ });
616
+ }
617
+ this.shouldScrollToBottom = true;
618
+ break;
619
+
620
+ case 'tool_use_end':
621
+ // 工具完成 - 更新状态
622
+ if (event.toolCall) {
623
+ const toolInfo = pendingTools.get(event.toolCall.id);
624
+ const duration = toolInfo ? Date.now() - toolInfo.startTime : 0;
625
+ const name = toolInfo?.name || event.toolCall.name || 'unknown';
626
+
627
+ aiMessage.content = aiMessage.content.replace(
628
+ new RegExp(`🔧 正在执行 ${name}\\.\\.\\.`),
629
+ `✅ ${name} (${duration}ms)`
630
+ );
631
+ pendingTools.delete(event.toolCall.id);
632
+ }
633
+ this.shouldScrollToBottom = true;
634
+ break;
635
+
636
+ case 'tool_result':
637
+ // 工具结果 - 存储用于最后显示
638
+ if (event.result) {
639
+ const preview = event.result.content.substring(0, 500);
640
+ const truncated = event.result.content.length > 500 ? '\n...(已截断)' : '';
641
+ toolResults.push(`\n\n📋 **输出**:\n\`\`\`\n${preview}${truncated}\n\`\`\``);
642
+ }
643
+ break;
644
+
645
+ case 'tool_error':
646
+ // 工具错误
647
+ aiMessage.content = aiMessage.content.replace(
648
+ /🔧 正在执行 \w+\.\.\./,
649
+ `❌ 工具执行失败: ${event.error}`
650
+ );
651
+ this.shouldScrollToBottom = true;
652
+ break;
653
+
654
+ case 'message_end':
655
+ // 消息结束 - 附加工具结果
656
+ if (toolResults.length > 0) {
657
+ aiMessage.content += toolResults.join('');
658
+ }
659
+ this.logger.info('Stream completed');
660
+ break;
661
+ }
662
+ },
663
+ error: (error) => {
664
+ this.logger.error('Stream error', error);
665
+ aiMessage.content += `\n\n❌ 错误: ${error instanceof Error ? error.message : 'Unknown error'}`;
666
+ this.isLoading = false;
667
+ this.shouldScrollToBottom = true;
668
+ this.saveChatHistory();
669
+ },
670
+ complete: () => {
671
+ this.isLoading = false;
672
+ this.updateTokenUsage();
673
+ this.saveChatHistory();
674
+ this.shouldScrollToBottom = true;
675
+ }
676
+ });
366
677
 
367
678
  } catch (error) {
368
679
  this.logger.error('Failed to send message', error);
369
680
 
370
681
  // 添加错误消息
371
- const errorMessage: ChatMessage = {
372
- id: this.generateId(),
373
- role: MessageRole.ASSISTANT,
374
- content: `抱歉,我遇到了一些问题:${error instanceof Error ? error.message : 'Unknown error'}\n\n请稍后重试。`,
375
- timestamp: new Date()
376
- };
377
- this.messages.push(errorMessage);
378
- } finally {
682
+ aiMessage.content = `抱歉,我遇到了一些问题:${error instanceof Error ? error.message : 'Unknown error'}\n\n请稍后重试。`;
379
683
  this.isLoading = false;
380
- // 更新 Token 使用情况
381
684
  this.updateTokenUsage();
382
- // 滚动到底部
383
685
  setTimeout(() => this.scrollToBottom(), 0);
384
686
  }
385
687
  }
@@ -600,12 +902,35 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
600
902
  }
601
903
 
602
904
  /**
603
- * 格式化消息内容(支持换行和基本格式化)
905
+ * 格式化消息内容(支持 Markdown 渲染)
604
906
  */
605
907
  formatMessage(content: string): string {
606
- return content
607
- .replace(/\n/g, '<br>')
608
- .replace(/•/g, '&#8226;');
908
+ if (!content) return '';
909
+
910
+ try {
911
+ // 使用 marked 库渲染 Markdown
912
+ const { marked } = require('marked');
913
+
914
+ // 配置 marked 选项
915
+ marked.setOptions({
916
+ breaks: true, // 支持换行
917
+ gfm: true, // 支持 GitHub Flavored Markdown
918
+ headerIds: false, // 不生成标题 ID
919
+ mangle: false // 不转义邮箱
920
+ });
921
+
922
+ return marked.parse(content);
923
+ } catch (e) {
924
+ // 如果 marked 失败,使用基本格式化
925
+ return content
926
+ .replace(/&/g, '&amp;')
927
+ .replace(/</g, '&lt;')
928
+ .replace(/>/g, '&gt;')
929
+ .replace(/\n/g, '<br>')
930
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
931
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
932
+ .replace(/`(.*?)`/g, '<code>$1</code>');
933
+ }
609
934
  }
610
935
 
611
936
  /**
@@ -649,7 +974,8 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
649
974
  submit(): void {
650
975
  const message = this.inputValue.trim();
651
976
  if (message && !this.isLoading) {
652
- this.onSendMessage(message);
977
+ // 使用 Agent 循环模式发送消息(支持多轮工具调用)
978
+ this.onSendMessageWithAgent(message);
653
979
  this.inputValue = '';
654
980
  setTimeout(() => this.autoResize(), 0);
655
981
  this.textInput?.nativeElement.focus();
@@ -687,4 +1013,13 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
687
1013
  isOverLimit(): boolean {
688
1014
  return this.getCharCount() > this.charLimit;
689
1015
  }
1016
+
1017
+ /**
1018
+ * 转义 HTML 特殊字符
1019
+ */
1020
+ private escapeHtml(text: string): string {
1021
+ const div = document.createElement('div');
1022
+ div.textContent = text;
1023
+ return div.innerHTML;
1024
+ }
690
1025
  }
@@ -2,6 +2,7 @@ import { Component, Output, EventEmitter, Input, ViewChild, ElementRef, OnInit,
2
2
  import { Subject } from 'rxjs';
3
3
  import { debounceTime, takeUntil } from 'rxjs/operators';
4
4
  import { ConfigProviderService } from '../../services/core/config-provider.service';
5
+ import { AiAssistantService } from '../../services/core/ai-assistant.service';
5
6
 
6
7
  @Component({
7
8
  selector: 'app-chat-input',
@@ -22,7 +23,14 @@ export class ChatInputComponent implements OnInit, OnDestroy {
22
23
  isComposing = false; // 用于处理中文输入法
23
24
  enterToSend: boolean = true; // Enter键发送
24
25
 
25
- constructor(private config: ConfigProviderService) {}
26
+ // 智能建议相关
27
+ suggestions: string[] = [];
28
+ showSuggestions = false;
29
+
30
+ constructor(
31
+ private config: ConfigProviderService,
32
+ private aiService: AiAssistantService
33
+ ) {}
26
34
 
27
35
  ngOnInit(): void {
28
36
  // 读取 Enter 发送设置
@@ -45,10 +53,34 @@ export class ChatInputComponent implements OnInit, OnDestroy {
45
53
 
46
54
  /**
47
55
  * 处理输入变化
56
+ * 实现智能建议功能
57
+ */
58
+ async onInputChange(value: string): Promise<void> {
59
+ if (value.length < 2) {
60
+ this.suggestions = [];
61
+ this.showSuggestions = false;
62
+ return;
63
+ }
64
+
65
+ // 调用已实现的智能建议服务
66
+ this.suggestions = await this.aiService.getSuggestedCommands(value);
67
+ this.showSuggestions = this.suggestions.length > 0;
68
+ }
69
+
70
+ /**
71
+ * 选择建议
72
+ */
73
+ selectSuggestion(suggestion: string): void {
74
+ this.inputValue = suggestion;
75
+ this.showSuggestions = false;
76
+ this.focus();
77
+ }
78
+
79
+ /**
80
+ * 关闭建议
48
81
  */
49
- onInputChange(value: string): void {
50
- // TODO: 实现智能建议功能
51
- // 可以基于输入内容提供命令建议
82
+ dismissSuggestions(): void {
83
+ this.showSuggestions = false;
52
84
  }
53
85
 
54
86
  /**