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, ViewEncapsulation } from '@angular/core';
2
2
  import { Subject } from 'rxjs';
3
3
  import { takeUntil } from 'rxjs/operators';
4
- import { ChatMessage, MessageRole, StreamEvent } 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';
@@ -191,7 +191,168 @@ export class ChatInterfaceComponent implements OnInit, OnDestroy, AfterViewCheck
191
191
  }
192
192
 
193
193
  /**
194
- * 处理发送消息
194
+ * 处理发送消息(使用 Agent 循环模式)
195
+ */
196
+ async onSendMessageWithAgent(content: string): Promise<void> {
197
+ if (!content.trim() || this.isLoading) {
198
+ return;
199
+ }
200
+
201
+ // 添加用户消息
202
+ const userMessage: ChatMessage = {
203
+ id: this.generateId(),
204
+ role: MessageRole.USER,
205
+ content: content.trim(),
206
+ timestamp: new Date()
207
+ };
208
+ this.messages.push(userMessage);
209
+
210
+ // 滚动到底部
211
+ setTimeout(() => this.scrollToBottom(), 0);
212
+
213
+ // 清空输入框
214
+ content = '';
215
+
216
+ // 显示加载状态
217
+ this.isLoading = true;
218
+
219
+ // 创建一个临时的 AI 消息用于流式更新
220
+ const aiMessage: ChatMessage = {
221
+ id: this.generateId(),
222
+ role: MessageRole.ASSISTANT,
223
+ content: '', // 初始为空
224
+ timestamp: new Date()
225
+ };
226
+ this.messages.push(aiMessage);
227
+
228
+ // 工具调用状态跟踪
229
+ const toolStatus = new Map<string, { name: string; startTime: number }>();
230
+
231
+ try {
232
+ // 使用 Agent 循环流式 API
233
+ this.aiService.chatStreamWithAgentLoop({
234
+ messages: this.messages.slice(0, -1), // 排除刚添加的空 AI 消息
235
+ maxTokens: 2000,
236
+ temperature: 0.7
237
+ }, {
238
+ maxRounds: 5
239
+ }).pipe(
240
+ takeUntil(this.destroy$)
241
+ ).subscribe({
242
+ next: (event: AgentStreamEvent) => {
243
+ switch (event.type) {
244
+ case 'text_delta':
245
+ // 流式显示文本
246
+ if (event.textDelta) {
247
+ aiMessage.content += event.textDelta;
248
+ this.shouldScrollToBottom = true;
249
+ }
250
+ break;
251
+
252
+ case 'tool_use_start':
253
+ // 显示工具开始
254
+ const toolName = event.toolCall?.name || 'unknown';
255
+ aiMessage.content += `\n\n🔧 ${this.t.chatInterface.executingTool} ${toolName}...`;
256
+ if (event.toolCall?.id) {
257
+ toolStatus.set(event.toolCall.id, {
258
+ name: toolName,
259
+ startTime: Date.now()
260
+ });
261
+ }
262
+ this.shouldScrollToBottom = true;
263
+ break;
264
+
265
+ case 'tool_executing':
266
+ // 工具正在执行(额外状态)
267
+ break;
268
+
269
+ case 'tool_executed':
270
+ // 工具执行完成 - 更新状态
271
+ if (event.toolCall && event.toolResult) {
272
+ const name = toolStatus.get(event.toolCall.id)?.name || event.toolCall.name || 'unknown';
273
+ const duration = event.toolResult.duration || 0;
274
+
275
+ // 替换等待提示为完成状态
276
+ aiMessage.content = aiMessage.content.replace(
277
+ new RegExp(`🔧 ${this.t.chatInterface.executingTool} ${name}\\.\\.\\.`),
278
+ `✅ ${name} (${duration}ms)`
279
+ );
280
+
281
+ // 显示工具输出预览
282
+ if (event.toolResult.content && !event.toolResult.is_error) {
283
+ const preview = event.toolResult.content.substring(0, 500);
284
+ const truncated = event.toolResult.content.length > 500 ? '...' : '';
285
+ aiMessage.content += `\n\n📋 **${this.t.chatInterface.toolOutput}**:\n\`\`\`\n${preview}${truncated}\n\`\`\``;
286
+ }
287
+
288
+ toolStatus.delete(event.toolCall.id);
289
+ }
290
+ this.shouldScrollToBottom = true;
291
+ break;
292
+
293
+ case 'tool_error':
294
+ // 工具执行失败
295
+ if (event.toolCall) {
296
+ const name = toolStatus.get(event.toolCall.id)?.name || event.toolCall.name || 'unknown';
297
+ aiMessage.content = aiMessage.content.replace(
298
+ new RegExp(`🔧 ${this.t.chatInterface.executingTool} ${name}\\.\\.\\.`),
299
+ `❌ ${name} ${this.t.chatInterface.toolFailed}: ${event.toolResult?.content || 'Unknown error'}`
300
+ );
301
+ toolStatus.delete(event.toolCall.id);
302
+ }
303
+ this.shouldScrollToBottom = true;
304
+ break;
305
+
306
+ case 'round_start':
307
+ // 新一轮开始
308
+ if (event.round && event.round > 1) {
309
+ aiMessage.content += '\n\n---\n\n';
310
+ }
311
+ break;
312
+
313
+ case 'round_end':
314
+ // 一轮结束
315
+ break;
316
+
317
+ case 'agent_complete':
318
+ // Agent 循环完成
319
+ this.logger.info('Agent completed', {
320
+ reason: event.reason,
321
+ totalRounds: event.totalRounds
322
+ });
323
+ break;
324
+
325
+ case 'error':
326
+ // 错误
327
+ aiMessage.content += `\n\n❌ ${this.t.chatInterface.errorPrefix}: ${event.error}`;
328
+ this.shouldScrollToBottom = true;
329
+ break;
330
+ }
331
+ },
332
+ error: (error) => {
333
+ this.logger.error('Agent stream error', error);
334
+ aiMessage.content += `\n\n❌ ${this.t.chatInterface.errorPrefix}: ${error instanceof Error ? error.message : 'Unknown error'}`;
335
+ this.isLoading = false;
336
+ this.shouldScrollToBottom = true;
337
+ this.saveChatHistory();
338
+ },
339
+ complete: () => {
340
+ this.isLoading = false;
341
+ this.saveChatHistory();
342
+ this.shouldScrollToBottom = true;
343
+ }
344
+ });
345
+
346
+ } catch (error) {
347
+ this.logger.error('Failed to send message with agent', error);
348
+ aiMessage.content = `${this.t.chatInterface.errorPrefix}: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${this.t.chatInterface.tipShortcut}`;
349
+ this.isLoading = false;
350
+ setTimeout(() => this.scrollToBottom(), 0);
351
+ }
352
+ }
353
+
354
+ /**
355
+ * 处理发送消息(原有方法,保留兼容性)
195
356
  */
196
357
  async onSendMessage(content: string): Promise<void> {
197
358
  if (!content.trim() || this.isLoading) {
@@ -225,6 +386,10 @@ export class ChatInterfaceComponent implements OnInit, OnDestroy, AfterViewCheck
225
386
  };
226
387
  this.messages.push(aiMessage);
227
388
 
389
+ // 工具调用状态跟踪
390
+ let pendingToolCalls: Map<string, { name: string; startTime: number }> = new Map();
391
+ let toolResultsToAppend: string[] = [];
392
+
228
393
  try {
229
394
  // 使用流式 API
230
395
  this.aiService.chatStream({
@@ -242,18 +407,73 @@ export class ChatInterfaceComponent implements OnInit, OnDestroy, AfterViewCheck
242
407
  }
243
408
  // 工具调用开始 - 显示提示
244
409
  else if (event.type === 'tool_use_start') {
245
- aiMessage.content += `\n\n🔧 ${this.t.chatInterface.executingTool}`;
410
+ const toolName = event.toolCall?.name ? ` (${event.toolCall.name})` : '';
411
+ aiMessage.content += `\n\n🔧 ${this.t.chatInterface.executingTool}${toolName}...`;
412
+
413
+ // 记录待执行的工具
414
+ if (event.toolCall?.id) {
415
+ pendingToolCalls.set(event.toolCall.id, {
416
+ name: event.toolCall.name || 'unknown',
417
+ startTime: Date.now()
418
+ });
419
+ }
246
420
  this.shouldScrollToBottom = true;
247
421
  }
248
422
  // 工具调用完成 - 更新状态
249
423
  else if (event.type === 'tool_use_end') {
250
- aiMessage.content = aiMessage.content.replace(`🔧 ${this.t.chatInterface.executingTool}`, `✅ ${this.t.chatInterface.toolComplete}`);
424
+ if (event.toolCall) {
425
+ const toolInfo = pendingToolCalls.get(event.toolCall.id);
426
+ const duration = toolInfo ? Date.now() - toolInfo.startTime : 0;
427
+ const toolName = toolInfo?.name || event.toolCall.name || 'unknown';
428
+
429
+ // 替换等待提示为完成状态
430
+ aiMessage.content = aiMessage.content.replace(
431
+ /🔧 正在执行工具.*?\.\.\./g,
432
+ `✅ ${toolName} 完成`
433
+ );
434
+
435
+ pendingToolCalls.delete(event.toolCall.id);
436
+ }
437
+ this.shouldScrollToBottom = true;
438
+ }
439
+ // 工具结果 - 追加到消息
440
+ else if (event.type === 'tool_result' && event.result) {
441
+ const isError = event.result.is_error;
442
+ const icon = isError ? '❌' : '📋';
443
+ const header = isError ? '**工具执行失败**' : '**工具输出**';
444
+
445
+ // 截断过长的结果
446
+ const maxPreviewLength = 800;
447
+ let resultPreview = event.result.content;
448
+ const isTruncated = resultPreview.length > maxPreviewLength;
449
+ if (isTruncated) {
450
+ resultPreview = resultPreview.substring(0, maxPreviewLength) + '\n...(已截断)';
451
+ }
452
+
453
+ // 格式化工具结果
454
+ const formattedResult = `\n\n${icon} ${header}:\n\`\`\`\n${resultPreview}\n\`\`\``;
455
+ toolResultsToAppend.push(formattedResult);
456
+ this.shouldScrollToBottom = true;
457
+ }
458
+ // 工具错误
459
+ else if (event.type === 'tool_error' && event.error) {
460
+ aiMessage.content = aiMessage.content.replace(
461
+ /🔧 正在执行工具.*?\.\.\./g,
462
+ `❌ 工具执行失败: ${event.error}`
463
+ );
251
464
  this.shouldScrollToBottom = true;
252
465
  }
253
- // 消息结束
466
+ // 消息结束 - 附加所有工具结果
254
467
  else if (event.type === 'message_end') {
255
468
  this.logger.info('Stream completed');
469
+
470
+ // 附加所有工具结果
471
+ if (toolResultsToAppend.length > 0) {
472
+ aiMessage.content += toolResultsToAppend.join('');
473
+ }
474
+
256
475
  this.playNotificationSound();
476
+ this.shouldScrollToBottom = true;
257
477
  }
258
478
  },
259
479
  error: (error) => {
@@ -1,5 +1,6 @@
1
1
  import { Component, Input, Output, EventEmitter, ViewEncapsulation } from '@angular/core';
2
2
  import { ChatMessage } from '../../types/ai.types';
3
+ import { ToastService } from '../../services/core/toast.service';
3
4
 
4
5
  @Component({
5
6
  selector: 'app-chat-message',
@@ -15,6 +16,8 @@ export class ChatMessageComponent {
15
16
  @Output() messageClick = new EventEmitter<ChatMessage>();
16
17
  @Output() messageAction = new EventEmitter<{ action: string; message: ChatMessage }>();
17
18
 
19
+ constructor(private toastService: ToastService) {}
20
+
18
21
  /**
19
22
  * 处理消息点击
20
23
  */
@@ -34,7 +37,9 @@ export class ChatMessageComponent {
34
37
  */
35
38
  copyMessage(): void {
36
39
  navigator.clipboard.writeText(this.message.content).then(() => {
37
- // TODO: 显示复制成功提示
40
+ this.toastService.success('已复制到剪贴板', 2000);
41
+ }).catch(error => {
42
+ this.toastService.error('复制失败,请重试');
38
43
  });
39
44
  }
40
45
 
@@ -1,91 +1,91 @@
1
- import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
2
- import { Subject } from 'rxjs';
3
- import { takeUntil } from 'rxjs/operators';
4
- import { ConfigProviderService } from '../../services/core/config-provider.service';
5
- import { ContextManager } from '../../services/context/manager';
6
- import { ToastService } from '../../services/core/toast.service';
7
- import { TranslateService } from '../../i18n';
8
- import { ContextConfig, DEFAULT_CONTEXT_CONFIG } from '../../types/ai.types';
9
-
10
- @Component({
11
- selector: 'app-context-settings',
12
- templateUrl: './context-settings.component.html',
13
- styleUrls: ['./context-settings.component.scss'],
14
- encapsulation: ViewEncapsulation.None
15
- })
16
- export class ContextSettingsComponent implements OnInit, OnDestroy {
17
- // 配置项
18
- config: ContextConfig = { ...DEFAULT_CONTEXT_CONFIG };
19
- autoCompactEnabled = true;
20
-
21
- // 当前供应商的上下文限制
22
- activeProviderContextWindow: number = 200000;
23
-
24
- // 翻译对象
25
- t: any;
26
-
27
- private destroy$ = new Subject<void>();
28
-
29
- constructor(
30
- private configService: ConfigProviderService,
31
- private contextManager: ContextManager,
32
- private toast: ToastService,
33
- private translate: TranslateService
34
- ) {
35
- this.t = this.translate.t;
36
- }
37
-
38
- ngOnInit(): void {
39
- this.translate.translation$.pipe(
40
- takeUntil(this.destroy$)
41
- ).subscribe(translation => {
42
- this.t = translation;
43
- });
44
-
45
- this.loadConfig();
46
- }
47
-
48
- ngOnDestroy(): void {
49
- this.destroy$.next();
50
- this.destroy$.complete();
51
- }
52
-
53
- loadConfig(): void {
54
- const savedConfig = this.configService.getContextConfig();
55
- if (savedConfig) {
56
- this.config = { ...DEFAULT_CONTEXT_CONFIG, ...savedConfig };
57
- }
58
- this.autoCompactEnabled = this.configService.isAutoCompactEnabled();
59
-
60
- // 获取当前供应商的上下文限制
61
- this.activeProviderContextWindow = this.configService.getActiveProviderContextWindow();
62
-
63
- // 确保配置的 maxContextTokens 不超过供应商限制
64
- if (this.config.maxContextTokens > this.activeProviderContextWindow) {
65
- this.config.maxContextTokens = this.activeProviderContextWindow;
66
- }
67
- }
68
-
69
- saveConfig(): void {
70
- this.configService.setContextConfig(this.config);
71
- this.contextManager.updateConfig(this.config);
72
- this.toast.success(this.t?.contextSettings?.configSaved || '上下文配置已保存');
73
- }
74
-
75
- toggleAutoCompact(): void {
76
- this.autoCompactEnabled = !this.autoCompactEnabled;
77
- this.configService.setAutoCompactEnabled(this.autoCompactEnabled);
78
- this.toast.info(
79
- this.autoCompactEnabled
80
- ? (this.t?.contextSettings?.autoCompactEnabled || '自动压缩已启用')
81
- : (this.t?.contextSettings?.autoCompactDisabled || '自动压缩已禁用')
82
- );
83
- }
84
-
85
- resetToDefaults(): void {
86
- this.config = { ...DEFAULT_CONTEXT_CONFIG };
87
- this.autoCompactEnabled = true;
88
- this.saveConfig();
89
- this.toast.info(this.t?.common?.resetToDefault || '已重置为默认配置');
90
- }
91
- }
1
+ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
2
+ import { Subject } from 'rxjs';
3
+ import { takeUntil } from 'rxjs/operators';
4
+ import { ConfigProviderService } from '../../services/core/config-provider.service';
5
+ import { ContextManager } from '../../services/context/manager';
6
+ import { ToastService } from '../../services/core/toast.service';
7
+ import { TranslateService } from '../../i18n';
8
+ import { ContextConfig, DEFAULT_CONTEXT_CONFIG } from '../../types/ai.types';
9
+
10
+ @Component({
11
+ selector: 'app-context-settings',
12
+ templateUrl: './context-settings.component.html',
13
+ styleUrls: ['./context-settings.component.scss'],
14
+ encapsulation: ViewEncapsulation.None
15
+ })
16
+ export class ContextSettingsComponent implements OnInit, OnDestroy {
17
+ // 配置项
18
+ config: ContextConfig = { ...DEFAULT_CONTEXT_CONFIG };
19
+ autoCompactEnabled = true;
20
+
21
+ // 当前供应商的上下文限制
22
+ activeProviderContextWindow: number = 200000;
23
+
24
+ // 翻译对象
25
+ t: any;
26
+
27
+ private destroy$ = new Subject<void>();
28
+
29
+ constructor(
30
+ private configService: ConfigProviderService,
31
+ private contextManager: ContextManager,
32
+ private toast: ToastService,
33
+ private translate: TranslateService
34
+ ) {
35
+ this.t = this.translate.t;
36
+ }
37
+
38
+ ngOnInit(): void {
39
+ this.translate.translation$.pipe(
40
+ takeUntil(this.destroy$)
41
+ ).subscribe(translation => {
42
+ this.t = translation;
43
+ });
44
+
45
+ this.loadConfig();
46
+ }
47
+
48
+ ngOnDestroy(): void {
49
+ this.destroy$.next();
50
+ this.destroy$.complete();
51
+ }
52
+
53
+ loadConfig(): void {
54
+ const savedConfig = this.configService.getContextConfig();
55
+ if (savedConfig) {
56
+ this.config = { ...DEFAULT_CONTEXT_CONFIG, ...savedConfig };
57
+ }
58
+ this.autoCompactEnabled = this.configService.isAutoCompactEnabled();
59
+
60
+ // 获取当前供应商的上下文限制
61
+ this.activeProviderContextWindow = this.configService.getActiveProviderContextWindow();
62
+
63
+ // 确保配置的 maxContextTokens 不超过供应商限制
64
+ if (this.config.maxContextTokens > this.activeProviderContextWindow) {
65
+ this.config.maxContextTokens = this.activeProviderContextWindow;
66
+ }
67
+ }
68
+
69
+ saveConfig(): void {
70
+ this.configService.setContextConfig(this.config);
71
+ this.contextManager.updateConfig(this.config);
72
+ this.toast.success(this.t?.contextSettings?.configSaved || '上下文配置已保存');
73
+ }
74
+
75
+ toggleAutoCompact(): void {
76
+ this.autoCompactEnabled = !this.autoCompactEnabled;
77
+ this.configService.setAutoCompactEnabled(this.autoCompactEnabled);
78
+ this.toast.info(
79
+ this.autoCompactEnabled
80
+ ? (this.t?.contextSettings?.autoCompactEnabled || '自动压缩已启用')
81
+ : (this.t?.contextSettings?.autoCompactDisabled || '自动压缩已禁用')
82
+ );
83
+ }
84
+
85
+ resetToDefaults(): void {
86
+ this.config = { ...DEFAULT_CONTEXT_CONFIG };
87
+ this.autoCompactEnabled = true;
88
+ this.saveConfig();
89
+ this.toast.info(this.t?.common?.resetToDefault || '已重置为默认配置');
90
+ }
91
+ }
@@ -1,4 +1,5 @@
1
1
  import { Component, Input } from '@angular/core';
2
+ import { AiSidebarService } from '../../services/chat/ai-sidebar.service';
2
3
 
3
4
  /**
4
5
  * AI工具栏按钮组件
@@ -14,8 +15,9 @@ export class AiToolbarButtonComponent {
14
15
  @Input() tooltip: string = 'Open AI Assistant';
15
16
  @Input() showLabel: boolean = true;
16
17
 
18
+ constructor(private sidebarService: AiSidebarService) {}
19
+
17
20
  onClick(): void {
18
- // TODO: 触发打开AI助手
19
- console.log('AI Assistant button clicked');
21
+ this.sidebarService.toggle();
20
22
  }
21
23
  }
@@ -1,7 +1,10 @@
1
1
  import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
2
2
  import { Subject } from 'rxjs';
3
3
  import { debounceTime, takeUntil } from 'rxjs/operators';
4
- import { CommandResponse } from '../../types/ai.types';
4
+ import { CommandResponse, CommandRequest } from '../../types/ai.types';
5
+ import { AiAssistantService } from '../../services/core/ai-assistant.service';
6
+ import { TerminalContextService } from '../../services/terminal/terminal-context.service';
7
+ import { LoggerService } from '../../services/core/logger.service';
5
8
 
6
9
  @Component({
7
10
  selector: 'app-command-suggestion',
@@ -14,12 +17,19 @@ export class CommandSuggestionComponent implements OnInit, OnDestroy {
14
17
  @Output() closed = new EventEmitter<void>();
15
18
 
16
19
  suggestions: CommandResponse[] = [];
20
+ isLoading = false;
17
21
  private inputSubject = new Subject<string>();
18
22
  private destroy$ = new Subject<void>();
19
23
 
24
+ constructor(
25
+ private aiService: AiAssistantService,
26
+ private terminalContext: TerminalContextService,
27
+ private logger: LoggerService
28
+ ) {}
29
+
20
30
  ngOnInit(): void {
21
31
  this.inputSubject.pipe(
22
- debounceTime(500),
32
+ debounceTime(300),
23
33
  takeUntil(this.destroy$)
24
34
  ).subscribe(text => {
25
35
  this.generateSuggestions(text);
@@ -35,10 +45,142 @@ export class CommandSuggestionComponent implements OnInit, OnDestroy {
35
45
  this.inputSubject.next(text);
36
46
  }
37
47
 
38
- private generateSuggestions(text: string): void {
39
- // TODO: 调用AI生成建议
40
- // 这里应该调用CommandGeneratorService
41
- this.suggestions = [];
48
+ private async generateSuggestions(text: string): Promise<void> {
49
+ // 如果输入太短,不生成建议
50
+ if (text.length < 2) {
51
+ this.suggestions = [];
52
+ return;
53
+ }
54
+
55
+ this.isLoading = true;
56
+
57
+ try {
58
+ // 1. 获取基于上下文的智能建议
59
+ const suggestedCommands = await this.aiService.getSuggestedCommands(text);
60
+
61
+ // 2. 如果有AI建议,转换为CommandResponse格式
62
+ const aiSuggestions: CommandResponse[] = [];
63
+
64
+ // 添加前5个最相关的建议
65
+ for (const cmd of suggestedCommands.slice(0, 5)) {
66
+ // 如果命令模板中包含占位符,用当前输入填充
67
+ const filledCmd = cmd.replace('""', text).replace('${input}', text);
68
+
69
+ aiSuggestions.push({
70
+ command: filledCmd,
71
+ explanation: this.getExplanationForCommand(filledCmd),
72
+ confidence: this.calculateConfidence(filledCmd, text)
73
+ });
74
+ }
75
+
76
+ // 3. 如果输入看起来像自然语言,调用AI生成命令
77
+ if (this.looksLikeNaturalLanguage(text)) {
78
+ const aiCommand = await this.generateAICommand(text);
79
+ if (aiCommand && !aiSuggestions.find(s => s.command === aiCommand.command)) {
80
+ aiSuggestions.unshift(aiCommand);
81
+ }
82
+ }
83
+
84
+ this.suggestions = aiSuggestions.slice(0, 6);
85
+
86
+ } catch (error) {
87
+ this.logger.error('Failed to generate suggestions', error);
88
+ this.suggestions = [];
89
+ } finally {
90
+ this.isLoading = false;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * 检查输入是否像自然语言
96
+ */
97
+ private looksLikeNaturalLanguage(text: string): boolean {
98
+ const naturalLanguagePatterns = [
99
+ /^[a-zA-Z]+/,
100
+ /^(帮我|请|我想|需要|要)/,
101
+ /how to|what is|what's/
102
+ ];
103
+
104
+ return naturalLanguagePatterns.some(pattern => pattern.test(text.toLowerCase()));
105
+ }
106
+
107
+ /**
108
+ * 调用AI生成命令
109
+ */
110
+ private async generateAICommand(naturalLanguage: string): Promise<CommandResponse | null> {
111
+ try {
112
+ const context = this.terminalContext.getCurrentContext();
113
+
114
+ const request: CommandRequest = {
115
+ naturalLanguage,
116
+ context: {
117
+ currentDirectory: context?.session.cwd,
118
+ operatingSystem: context?.systemInfo.platform,
119
+ shell: context?.session.shell,
120
+ environment: context?.session.environment
121
+ }
122
+ };
123
+
124
+ const response = await this.aiService.generateCommand(request);
125
+ return response;
126
+
127
+ } catch (error) {
128
+ this.logger.warn('AI command generation failed', error);
129
+ return null;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * 为命令生成解释
135
+ */
136
+ private getExplanationForCommand(command: string): string {
137
+ const explanations: { [key: string]: string } = {
138
+ 'git status': '查看当前Git仓库状态',
139
+ 'git pull': '拉取远程最新代码',
140
+ 'git add .': '添加所有文件到暂存区',
141
+ 'git commit -m ""': '提交暂存区文件',
142
+ 'git checkout -b ': '创建并切换到新分支',
143
+ 'git log --oneline': '查看简化的提交历史',
144
+ 'npm install': '安装项目依赖',
145
+ 'npm run dev': '启动开发服务器',
146
+ 'npm run build': '构建生产版本',
147
+ 'npm test': '运行测试',
148
+ 'docker build -t .': '构建Docker镜像',
149
+ 'docker-compose up': '启动Docker容器',
150
+ 'docker ps': '查看运行中的容器',
151
+ 'kubectl get pods': '查看Kubernetes Pods',
152
+ 'ls -la': '列出所有文件详细信息',
153
+ 'grep -r "" .': '递归搜索文件内容'
154
+ };
155
+
156
+ return explanations[command] || '执行命令';
157
+ }
158
+
159
+ /**
160
+ * 计算命令匹配度
161
+ */
162
+ private calculateConfidence(command: string, input: string): number {
163
+ const lowerInput = input.toLowerCase();
164
+ const lowerCmd = command.toLowerCase();
165
+
166
+ // 完全匹配开头
167
+ if (lowerCmd.startsWith(lowerInput)) {
168
+ return 0.95;
169
+ }
170
+
171
+ // 包含输入关键词
172
+ if (lowerCmd.includes(lowerInput)) {
173
+ return 0.8;
174
+ }
175
+
176
+ // 相同的命令前缀
177
+ const inputParts = lowerInput.split(' ');
178
+ const cmdParts = lowerCmd.split(' ');
179
+ if (inputParts[0] === cmdParts[0]) {
180
+ return 0.7;
181
+ }
182
+
183
+ return 0.5;
42
184
  }
43
185
 
44
186
  selectSuggestion(suggestion: CommandResponse): void {