tabby-ai-assistant 1.0.13 → 1.0.16

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 (42) hide show
  1. package/.editorconfig +18 -0
  2. package/README.md +40 -10
  3. package/dist/index.js +1 -1
  4. package/package.json +5 -3
  5. package/src/components/chat/ai-sidebar.component.scss +220 -9
  6. package/src/components/chat/ai-sidebar.component.ts +379 -29
  7. package/src/components/chat/chat-input.component.ts +36 -4
  8. package/src/components/chat/chat-interface.component.ts +225 -5
  9. package/src/components/chat/chat-message.component.ts +6 -1
  10. package/src/components/settings/context-settings.component.ts +91 -91
  11. package/src/components/terminal/ai-toolbar-button.component.ts +4 -2
  12. package/src/components/terminal/command-suggestion.component.ts +148 -6
  13. package/src/index.ts +81 -19
  14. package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
  15. package/src/services/chat/ai-sidebar.service.ts +448 -410
  16. package/src/services/chat/chat-session.service.ts +36 -12
  17. package/src/services/context/compaction.ts +110 -134
  18. package/src/services/context/manager.ts +27 -7
  19. package/src/services/context/memory.ts +17 -33
  20. package/src/services/context/summary.service.ts +136 -0
  21. package/src/services/core/ai-assistant.service.ts +1060 -37
  22. package/src/services/core/ai-provider-manager.service.ts +154 -25
  23. package/src/services/core/checkpoint.service.ts +218 -18
  24. package/src/services/core/toast.service.ts +106 -106
  25. package/src/services/providers/anthropic-provider.service.ts +126 -30
  26. package/src/services/providers/base-provider.service.ts +90 -7
  27. package/src/services/providers/glm-provider.service.ts +151 -38
  28. package/src/services/providers/minimax-provider.service.ts +55 -40
  29. package/src/services/providers/ollama-provider.service.ts +117 -28
  30. package/src/services/providers/openai-compatible.service.ts +164 -34
  31. package/src/services/providers/openai-provider.service.ts +169 -34
  32. package/src/services/providers/vllm-provider.service.ts +116 -28
  33. package/src/services/terminal/terminal-context.service.ts +265 -5
  34. package/src/services/terminal/terminal-manager.service.ts +845 -748
  35. package/src/services/terminal/terminal-tools.service.ts +612 -441
  36. package/src/types/ai.types.ts +156 -3
  37. package/src/utils/cost.utils.ts +249 -0
  38. package/src/utils/validation.utils.ts +306 -2
  39. package/dist/index.js.LICENSE.txt +0 -18
  40. package/src/services/terminal/command-analyzer.service.ts +0 -43
  41. package/src/services/terminal/context-menu.service.ts +0 -45
  42. 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';
@@ -220,6 +220,21 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
220
220
  this.sendWelcomeMessage();
221
221
  }
222
222
 
223
+ // 订阅预设消息(快捷键功能)
224
+ this.sidebarService.presetMessage$.pipe(
225
+ takeUntil(this.destroy$)
226
+ ).subscribe(({ message, autoSend }) => {
227
+ this.inputValue = message;
228
+
229
+ if (autoSend) {
230
+ // 延迟一点确保 UI 更新
231
+ setTimeout(() => this.submit(), 100);
232
+ } else {
233
+ // 聚焦输入框
234
+ this.textInput?.nativeElement?.focus();
235
+ }
236
+ });
237
+
223
238
  // 延迟检查滚动状态(等待 DOM 渲染)
224
239
  setTimeout(() => this.checkScrollState(), 100);
225
240
  }
@@ -325,9 +340,10 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
325
340
  }
326
341
 
327
342
  /**
328
- * 处理发送消息
343
+ * 处理发送消息 - 使用 Agent 循环模式
344
+ * 支持多轮工具调用自动循环
329
345
  */
330
- async onSendMessage(content: string): Promise<void> {
346
+ async onSendMessageWithAgent(content: string): Promise<void> {
331
347
  if (!content.trim() || this.isLoading) {
332
348
  return;
333
349
  }
@@ -344,42 +360,343 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
344
360
  // 滚动到底部
345
361
  setTimeout(() => this.scrollToBottom(), 0);
346
362
 
347
- // 清空输入框
348
- content = '';
349
-
350
363
  // 显示加载状态
351
364
  this.isLoading = true;
352
365
 
366
+ // 创建 AI 消息占位符用于流式更新
367
+ const aiMessage: ChatMessage = {
368
+ id: this.generateId(),
369
+ role: MessageRole.ASSISTANT,
370
+ content: '',
371
+ timestamp: new Date()
372
+ };
373
+ this.messages.push(aiMessage);
374
+
375
+ // 工具调用跟踪
376
+ const toolStatus = new Map<string, { name: string; startTime: number }>();
377
+
353
378
  try {
354
- // 发送请求到AI
355
- const response = await this.aiService.chat({
356
- messages: this.messages,
357
- maxTokens: 1000,
379
+ // 使用 Agent 循环流式 API
380
+ this.aiService.chatStreamWithAgentLoop({
381
+ messages: this.messages.slice(0, -1),
382
+ maxTokens: 2000,
358
383
  temperature: 0.7
384
+ }, {
385
+ maxRounds: 5
386
+ }).pipe(
387
+ takeUntil(this.destroy$)
388
+ ).subscribe({
389
+ next: (event: AgentStreamEvent) => {
390
+ switch (event.type) {
391
+ case 'text_delta':
392
+ // 文本流式显示
393
+ if (event.textDelta) {
394
+ aiMessage.content += event.textDelta;
395
+ this.shouldScrollToBottom = true;
396
+ }
397
+ break;
398
+
399
+ case 'tool_use_start':
400
+ // 工具开始 - 使用专门的工具卡片样式
401
+ const toolName = event.toolCall?.name || 'unknown';
402
+ aiMessage.content += `
403
+ <div class="tool-call-card tool-executing">
404
+ <div class="tool-header">
405
+ <span class="tool-icon">🔧</span>
406
+ <span class="tool-name">${toolName}</span>
407
+ <span class="tool-status" id="tool-status-${event.toolCall?.id}">执行中...</span>
408
+ </div>
409
+ </div>`;
410
+ if (event.toolCall?.id) {
411
+ toolStatus.set(event.toolCall.id, {
412
+ name: toolName,
413
+ startTime: Date.now()
414
+ });
415
+ }
416
+ this.shouldScrollToBottom = true;
417
+ break;
418
+
419
+ case 'tool_executing':
420
+ // 工具正在执行(额外状态)
421
+ break;
422
+
423
+ case 'tool_executed':
424
+ // 工具完成 - 更新工具卡片
425
+ if (event.toolCall && event.toolResult) {
426
+ const name = toolStatus.get(event.toolCall.id)?.name || event.toolCall.name || 'unknown';
427
+ const duration = event.toolResult.duration || 0;
428
+
429
+ // 更新工具卡片状态
430
+ const executingCardRegex = new RegExp(
431
+ `<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>`,
432
+ 'g'
433
+ );
434
+
435
+ // 构建工具结果卡片
436
+ let toolCard = `
437
+ <div class="tool-call-card tool-success">
438
+ <div class="tool-header">
439
+ <span class="tool-icon">✅</span>
440
+ <span class="tool-name">${name}</span>
441
+ <span class="tool-duration">${duration}ms</span>
442
+ </div>`;
443
+
444
+ // 添加工具输出
445
+ if (event.toolResult.content && !event.toolResult.is_error) {
446
+ const preview = event.toolResult.content.substring(0, 500);
447
+ const truncated = event.toolResult.content.length > 500 ? '...' : '';
448
+ toolCard += `
449
+ <div class="tool-output">
450
+ <div class="tool-output-header">输出:</div>
451
+ <pre class="tool-output-content">${this.escapeHtml(preview)}${truncated}</pre>
452
+ </div>`;
453
+ }
454
+
455
+ toolCard += `</div>`;
456
+
457
+ aiMessage.content = aiMessage.content.replace(executingCardRegex, toolCard);
458
+ toolStatus.delete(event.toolCall.id);
459
+ }
460
+ this.shouldScrollToBottom = true;
461
+ break;
462
+
463
+ case 'tool_error':
464
+ // 工具错误 - 更新工具卡片为错误状态
465
+ if (event.toolCall) {
466
+ const name = toolStatus.get(event.toolCall.id)?.name || event.toolCall.name || 'unknown';
467
+ const errorMsg = event.toolResult?.content || 'Unknown error';
468
+
469
+ const executingCardRegex = new RegExp(
470
+ `<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>`,
471
+ 'g'
472
+ );
473
+
474
+ const errorCard = `
475
+ <div class="tool-call-card tool-error">
476
+ <div class="tool-header">
477
+ <span class="tool-icon">❌</span>
478
+ <span class="tool-name">${name}</span>
479
+ <span class="tool-status">失败</span>
480
+ </div>
481
+ <div class="tool-output tool-error-message">
482
+ <pre class="tool-output-content">${this.escapeHtml(errorMsg)}</pre>
483
+ </div>
484
+ </div>`;
485
+
486
+ aiMessage.content = aiMessage.content.replace(executingCardRegex, errorCard);
487
+ toolStatus.delete(event.toolCall.id);
488
+ }
489
+ this.shouldScrollToBottom = true;
490
+ break;
491
+
492
+ case 'round_start':
493
+ // 新一轮开始
494
+ if (event.round && event.round > 1) {
495
+ aiMessage.content += '\n\n---\n\n';
496
+ }
497
+ break;
498
+
499
+ case 'round_end':
500
+ // 一轮结束
501
+ break;
502
+
503
+ case 'agent_complete':
504
+ // Agent 循环完成 - 显示终止原因
505
+ this.logger.info('Agent completed', {
506
+ reason: event.reason,
507
+ totalRounds: event.totalRounds
508
+ });
509
+
510
+ // 终止原因映射
511
+ const reasonText: Record<string, { icon: string; label: string }> = {
512
+ 'task_complete': { icon: '✅', label: '任务完成' },
513
+ 'no_tools': { icon: '✅', label: '已执行完成' },
514
+ 'summarizing': { icon: '✅', label: '总结完成' },
515
+ 'repeated_tool': { icon: '⚠️', label: '检测到重复操作' },
516
+ 'high_failure_rate': { icon: '⚠️', label: '多次调用失败' },
517
+ 'timeout': { icon: '⏱️', label: '执行超时' },
518
+ 'max_rounds': { icon: '⚠️', label: '达到最大轮数' },
519
+ 'user_cancel': { icon: '🛑', label: '用户取消' }
520
+ };
521
+
522
+ const reasonInfo = reasonText[event.reason || ''] || { icon: '📌', label: '完成' };
523
+ const roundsText = event.totalRounds ? ` (${event.totalRounds} 轮)` : '';
524
+
525
+ // 添加终止信息
526
+ aiMessage.content += `\n\n---\n**${reasonInfo.icon} ${reasonInfo.label}**${roundsText}`;
527
+
528
+ // 如果有终止消息,添加到内容中
529
+ if (event.terminationMessage) {
530
+ aiMessage.content += `\n${event.terminationMessage}`;
531
+ }
532
+
533
+ this.shouldScrollToBottom = true;
534
+ break;
535
+
536
+ case 'error':
537
+ // 错误
538
+ aiMessage.content += `\n\n❌ 错误: ${event.error}`;
539
+ this.shouldScrollToBottom = true;
540
+ break;
541
+ }
542
+ },
543
+ error: (error) => {
544
+ this.logger.error('Agent stream error', error);
545
+ aiMessage.content += `\n\n❌ 错误: ${error instanceof Error ? error.message : 'Unknown error'}`;
546
+ this.isLoading = false;
547
+ this.shouldScrollToBottom = true;
548
+ this.saveChatHistory();
549
+ },
550
+ complete: () => {
551
+ this.isLoading = false;
552
+ this.updateTokenUsage();
553
+ this.saveChatHistory();
554
+ this.shouldScrollToBottom = true;
555
+ }
359
556
  });
360
557
 
361
- // 添加AI响应
362
- this.messages.push(response.message);
558
+ } catch (error) {
559
+ this.logger.error('Failed to send message with agent', error);
560
+ aiMessage.content = `抱歉,我遇到了一些问题:${error instanceof Error ? error.message : 'Unknown error'}\n\n请稍后重试。`;
561
+ this.isLoading = false;
562
+ this.updateTokenUsage();
563
+ setTimeout(() => this.scrollToBottom(), 0);
564
+ }
565
+ }
566
+
567
+ /**
568
+ * 处理发送消息 - 原有方法(保留兼容性)
569
+ */
570
+ async onSendMessage(content: string): Promise<void> {
571
+ if (!content.trim() || this.isLoading) {
572
+ return;
573
+ }
574
+
575
+ // 添加用户消息
576
+ const userMessage: ChatMessage = {
577
+ id: this.generateId(),
578
+ role: MessageRole.USER,
579
+ content: content.trim(),
580
+ timestamp: new Date()
581
+ };
582
+ this.messages.push(userMessage);
583
+
584
+ // 滚动到底部
585
+ setTimeout(() => this.scrollToBottom(), 0);
586
+
587
+ // 显示加载状态
588
+ this.isLoading = true;
589
+
590
+ // 创建 AI 消息占位符用于流式更新
591
+ const aiMessage: ChatMessage = {
592
+ id: this.generateId(),
593
+ role: MessageRole.ASSISTANT,
594
+ content: '',
595
+ timestamp: new Date()
596
+ };
597
+ this.messages.push(aiMessage);
598
+
599
+ // 工具调用跟踪
600
+ const pendingTools = new Map<string, { name: string; startTime: number }>();
601
+ const toolResults: string[] = [];
363
602
 
364
- // 保存聊天历史
365
- this.saveChatHistory();
603
+ try {
604
+ // 使用流式 API
605
+ this.aiService.chatStream({
606
+ messages: this.messages.slice(0, -1), // 排除刚添加的空 AI 消息
607
+ maxTokens: 2000,
608
+ temperature: 0.7
609
+ }).pipe(
610
+ takeUntil(this.destroy$)
611
+ ).subscribe({
612
+ next: (event: StreamEvent) => {
613
+ switch (event.type) {
614
+ case 'text_delta':
615
+ // 文本流式显示
616
+ if (event.textDelta) {
617
+ aiMessage.content += event.textDelta;
618
+ this.shouldScrollToBottom = true;
619
+ }
620
+ break;
621
+
622
+ case 'tool_use_start':
623
+ // 工具开始 - 显示工具名称
624
+ const toolName = event.toolCall?.name || 'unknown';
625
+ aiMessage.content += `\n\n🔧 正在执行 ${toolName}...`;
626
+ if (event.toolCall?.id) {
627
+ pendingTools.set(event.toolCall.id, {
628
+ name: toolName,
629
+ startTime: Date.now()
630
+ });
631
+ }
632
+ this.shouldScrollToBottom = true;
633
+ break;
634
+
635
+ case 'tool_use_end':
636
+ // 工具完成 - 更新状态
637
+ if (event.toolCall) {
638
+ const toolInfo = pendingTools.get(event.toolCall.id);
639
+ const duration = toolInfo ? Date.now() - toolInfo.startTime : 0;
640
+ const name = toolInfo?.name || event.toolCall.name || 'unknown';
641
+
642
+ aiMessage.content = aiMessage.content.replace(
643
+ new RegExp(`🔧 正在执行 ${name}\\.\\.\\.`),
644
+ `✅ ${name} (${duration}ms)`
645
+ );
646
+ pendingTools.delete(event.toolCall.id);
647
+ }
648
+ this.shouldScrollToBottom = true;
649
+ break;
650
+
651
+ case 'tool_result':
652
+ // 工具结果 - 存储用于最后显示
653
+ if (event.result) {
654
+ const preview = event.result.content.substring(0, 500);
655
+ const truncated = event.result.content.length > 500 ? '\n...(已截断)' : '';
656
+ toolResults.push(`\n\n📋 **输出**:\n\`\`\`\n${preview}${truncated}\n\`\`\``);
657
+ }
658
+ break;
659
+
660
+ case 'tool_error':
661
+ // 工具错误
662
+ aiMessage.content = aiMessage.content.replace(
663
+ /🔧 正在执行 \w+\.\.\./,
664
+ `❌ 工具执行失败: ${event.error}`
665
+ );
666
+ this.shouldScrollToBottom = true;
667
+ break;
668
+
669
+ case 'message_end':
670
+ // 消息结束 - 附加工具结果
671
+ if (toolResults.length > 0) {
672
+ aiMessage.content += toolResults.join('');
673
+ }
674
+ this.logger.info('Stream completed');
675
+ break;
676
+ }
677
+ },
678
+ error: (error) => {
679
+ this.logger.error('Stream error', error);
680
+ aiMessage.content += `\n\n❌ 错误: ${error instanceof Error ? error.message : 'Unknown error'}`;
681
+ this.isLoading = false;
682
+ this.shouldScrollToBottom = true;
683
+ this.saveChatHistory();
684
+ },
685
+ complete: () => {
686
+ this.isLoading = false;
687
+ this.updateTokenUsage();
688
+ this.saveChatHistory();
689
+ this.shouldScrollToBottom = true;
690
+ }
691
+ });
366
692
 
367
693
  } catch (error) {
368
694
  this.logger.error('Failed to send message', error);
369
695
 
370
696
  // 添加错误消息
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 {
697
+ aiMessage.content = `抱歉,我遇到了一些问题:${error instanceof Error ? error.message : 'Unknown error'}\n\n请稍后重试。`;
379
698
  this.isLoading = false;
380
- // 更新 Token 使用情况
381
699
  this.updateTokenUsage();
382
- // 滚动到底部
383
700
  setTimeout(() => this.scrollToBottom(), 0);
384
701
  }
385
702
  }
@@ -600,12 +917,35 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
600
917
  }
601
918
 
602
919
  /**
603
- * 格式化消息内容(支持换行和基本格式化)
920
+ * 格式化消息内容(支持 Markdown 渲染)
604
921
  */
605
922
  formatMessage(content: string): string {
606
- return content
607
- .replace(/\n/g, '<br>')
608
- .replace(/•/g, '&#8226;');
923
+ if (!content) return '';
924
+
925
+ try {
926
+ // 使用 marked 库渲染 Markdown
927
+ const { marked } = require('marked');
928
+
929
+ // 配置 marked 选项
930
+ marked.setOptions({
931
+ breaks: true, // 支持换行
932
+ gfm: true, // 支持 GitHub Flavored Markdown
933
+ headerIds: false, // 不生成标题 ID
934
+ mangle: false // 不转义邮箱
935
+ });
936
+
937
+ return marked.parse(content);
938
+ } catch (e) {
939
+ // 如果 marked 失败,使用基本格式化
940
+ return content
941
+ .replace(/&/g, '&amp;')
942
+ .replace(/</g, '&lt;')
943
+ .replace(/>/g, '&gt;')
944
+ .replace(/\n/g, '<br>')
945
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
946
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
947
+ .replace(/`(.*?)`/g, '<code>$1</code>');
948
+ }
609
949
  }
610
950
 
611
951
  /**
@@ -649,7 +989,8 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
649
989
  submit(): void {
650
990
  const message = this.inputValue.trim();
651
991
  if (message && !this.isLoading) {
652
- this.onSendMessage(message);
992
+ // 使用 Agent 循环模式发送消息(支持多轮工具调用)
993
+ this.onSendMessageWithAgent(message);
653
994
  this.inputValue = '';
654
995
  setTimeout(() => this.autoResize(), 0);
655
996
  this.textInput?.nativeElement.focus();
@@ -687,4 +1028,13 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
687
1028
  isOverLimit(): boolean {
688
1029
  return this.getCharCount() > this.charLimit;
689
1030
  }
1031
+
1032
+ /**
1033
+ * 转义 HTML 特殊字符
1034
+ */
1035
+ private escapeHtml(text: string): string {
1036
+ const div = document.createElement('div');
1037
+ div.textContent = text;
1038
+ return div.innerHTML;
1039
+ }
690
1040
  }
@@ -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
  /**