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,106 +1,106 @@
1
- import { Injectable } from '@angular/core';
2
- import { Subject } from 'rxjs';
3
-
4
- export interface ToastMessage {
5
- id: string;
6
- type: 'success' | 'error' | 'warning' | 'info';
7
- message: string;
8
- duration?: number;
9
- }
10
-
11
- @Injectable({ providedIn: 'root' })
12
- export class ToastService {
13
- private toastSubject = new Subject<ToastMessage>();
14
- toast$ = this.toastSubject.asObservable();
15
-
16
- show(type: ToastMessage['type'], message: string, duration = 3000): void {
17
- const id = `toast-${Date.now()}`;
18
-
19
- // 确保容器存在
20
- let container = document.getElementById('ai-toast-container');
21
- if (!container) {
22
- container = document.createElement('div');
23
- container.id = 'ai-toast-container';
24
- container.className = 'ai-toast-container';
25
- container.style.cssText = `
26
- position: fixed;
27
- bottom: 20px;
28
- right: 20px;
29
- z-index: 99999;
30
- display: flex;
31
- flex-direction: column;
32
- gap: 10px;
33
- pointer-events: none;
34
- `;
35
- document.body.appendChild(container);
36
- }
37
-
38
- // 创建 Toast 元素
39
- const toast = document.createElement('div');
40
- toast.id = id;
41
- toast.className = `ai-toast ai-toast-${type}`;
42
- toast.style.cssText = `
43
- padding: 12px 16px;
44
- border-radius: 8px;
45
- color: white;
46
- font-size: 14px;
47
- display: flex;
48
- align-items: center;
49
- gap: 8px;
50
- animation: toastSlideIn 0.3s ease;
51
- cursor: pointer;
52
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
53
- pointer-events: auto;
54
- min-width: 200px;
55
- max-width: 350px;
56
- ${type === 'success' ? 'background: linear-gradient(135deg, #22c55e, #16a34a);' : ''}
57
- ${type === 'error' ? 'background: linear-gradient(135deg, #ef4444, #dc2626);' : ''}
58
- ${type === 'warning' ? 'background: linear-gradient(135deg, #f59e0b, #d97706);' : ''}
59
- ${type === 'info' ? 'background: linear-gradient(135deg, #3b82f6, #2563eb);' : ''}
60
- `;
61
-
62
- const icon = type === 'success' ? '✓' : type === 'error' ? '✗' : type === 'warning' ? '⚠' : 'ℹ';
63
- toast.innerHTML = `<span style="font-size: 16px;">${icon}</span><span>${message}</span>`;
64
-
65
- toast.onclick = () => {
66
- this.removeToast(toast);
67
- };
68
-
69
- container.appendChild(toast);
70
-
71
- // 自动消失
72
- setTimeout(() => {
73
- this.removeToast(toast);
74
- }, duration);
75
-
76
- // 发射事件(兼容现有订阅)
77
- this.toastSubject.next({ id, type, message, duration });
78
- }
79
-
80
- private removeToast(toast: HTMLElement): void {
81
- if (toast && toast.parentNode) {
82
- toast.style.animation = 'toastSlideOut 0.3s ease forwards';
83
- setTimeout(() => {
84
- if (toast.parentNode) {
85
- toast.remove();
86
- }
87
- }, 300);
88
- }
89
- }
90
-
91
- success(message: string, duration = 3000): void {
92
- this.show('success', message, duration);
93
- }
94
-
95
- error(message: string, duration = 5000): void {
96
- this.show('error', message, duration);
97
- }
98
-
99
- warning(message: string, duration = 4000): void {
100
- this.show('warning', message, duration);
101
- }
102
-
103
- info(message: string, duration = 3000): void {
104
- this.show('info', message, duration);
105
- }
106
- }
1
+ import { Injectable } from '@angular/core';
2
+ import { Subject } from 'rxjs';
3
+
4
+ export interface ToastMessage {
5
+ id: string;
6
+ type: 'success' | 'error' | 'warning' | 'info';
7
+ message: string;
8
+ duration?: number;
9
+ }
10
+
11
+ @Injectable({ providedIn: 'root' })
12
+ export class ToastService {
13
+ private toastSubject = new Subject<ToastMessage>();
14
+ toast$ = this.toastSubject.asObservable();
15
+
16
+ show(type: ToastMessage['type'], message: string, duration = 3000): void {
17
+ const id = `toast-${Date.now()}`;
18
+
19
+ // 确保容器存在
20
+ let container = document.getElementById('ai-toast-container');
21
+ if (!container) {
22
+ container = document.createElement('div');
23
+ container.id = 'ai-toast-container';
24
+ container.className = 'ai-toast-container';
25
+ container.style.cssText = `
26
+ position: fixed;
27
+ bottom: 20px;
28
+ right: 20px;
29
+ z-index: 99999;
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: 10px;
33
+ pointer-events: none;
34
+ `;
35
+ document.body.appendChild(container);
36
+ }
37
+
38
+ // 创建 Toast 元素
39
+ const toast = document.createElement('div');
40
+ toast.id = id;
41
+ toast.className = `ai-toast ai-toast-${type}`;
42
+ toast.style.cssText = `
43
+ padding: 12px 16px;
44
+ border-radius: 8px;
45
+ color: white;
46
+ font-size: 14px;
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 8px;
50
+ animation: toastSlideIn 0.3s ease;
51
+ cursor: pointer;
52
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
53
+ pointer-events: auto;
54
+ min-width: 200px;
55
+ max-width: 350px;
56
+ ${type === 'success' ? 'background: linear-gradient(135deg, #22c55e, #16a34a);' : ''}
57
+ ${type === 'error' ? 'background: linear-gradient(135deg, #ef4444, #dc2626);' : ''}
58
+ ${type === 'warning' ? 'background: linear-gradient(135deg, #f59e0b, #d97706);' : ''}
59
+ ${type === 'info' ? 'background: linear-gradient(135deg, #3b82f6, #2563eb);' : ''}
60
+ `;
61
+
62
+ const icon = type === 'success' ? '✓' : type === 'error' ? '✗' : type === 'warning' ? '⚠' : 'ℹ';
63
+ toast.innerHTML = `<span style="font-size: 16px;">${icon}</span><span>${message}</span>`;
64
+
65
+ toast.onclick = () => {
66
+ this.removeToast(toast);
67
+ };
68
+
69
+ container.appendChild(toast);
70
+
71
+ // 自动消失
72
+ setTimeout(() => {
73
+ this.removeToast(toast);
74
+ }, duration);
75
+
76
+ // 发射事件(兼容现有订阅)
77
+ this.toastSubject.next({ id, type, message, duration });
78
+ }
79
+
80
+ private removeToast(toast: HTMLElement): void {
81
+ if (toast && toast.parentNode) {
82
+ toast.style.animation = 'toastSlideOut 0.3s ease forwards';
83
+ setTimeout(() => {
84
+ if (toast.parentNode) {
85
+ toast.remove();
86
+ }
87
+ }, 300);
88
+ }
89
+ }
90
+
91
+ success(message: string, duration = 3000): void {
92
+ this.show('success', message, duration);
93
+ }
94
+
95
+ error(message: string, duration = 5000): void {
96
+ this.show('error', message, duration);
97
+ }
98
+
99
+ warning(message: string, duration = 4000): void {
100
+ this.show('warning', message, duration);
101
+ }
102
+
103
+ info(message: string, duration = 3000): void {
104
+ this.show('info', message, duration);
105
+ }
106
+ }
@@ -1,9 +1,9 @@
1
1
  import { Injectable } from '@angular/core';
2
- import { Observable, from } from 'rxjs';
2
+ import { Observable, Observer } from 'rxjs';
3
3
  import { Anthropic } from '@anthropic-ai/sdk';
4
4
  import { BaseAiProvider } from './base-provider.service';
5
- import { ProviderCapability, HealthStatus, ValidationResult } from '../../types/provider.types';
6
- import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, MessageRole } from '../../types/ai.types';
5
+ import { ProviderCapability, ValidationResult } from '../../types/provider.types';
6
+ import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, MessageRole, StreamEvent } from '../../types/ai.types';
7
7
  import { LoggerService } from '../core/logger.service';
8
8
 
9
9
  /**
@@ -93,11 +93,119 @@ export class AnthropicProviderService extends BaseAiProvider {
93
93
  }
94
94
 
95
95
  /**
96
- * 流式聊天功能 - 暂未实现,回退到非流式
96
+ * 流式聊天功能 - 支持工具调用事件
97
97
  */
98
- chatStream(request: ChatRequest): Observable<any> {
99
- // 回退到非流式
100
- return from(this.chat(request));
98
+ chatStream(request: ChatRequest): Observable<StreamEvent> {
99
+ return new Observable<StreamEvent>((subscriber: Observer<StreamEvent>) => {
100
+ if (!this.client) {
101
+ const error = new Error('Anthropic client not initialized');
102
+ subscriber.next({ type: 'error', error: error.message });
103
+ subscriber.error(error);
104
+ return;
105
+ }
106
+
107
+ let currentToolId = '';
108
+ let currentToolName = '';
109
+ let currentToolInput = '';
110
+ let fullContent = '';
111
+
112
+ const abortController = new AbortController();
113
+
114
+ const runStream = async () => {
115
+ try {
116
+ const stream = await this.client!.messages.stream({
117
+ model: this.config?.model || 'claude-3-sonnet',
118
+ max_tokens: request.maxTokens || 1000,
119
+ system: request.systemPrompt || this.getDefaultSystemPrompt(),
120
+ messages: this.transformMessages(request.messages),
121
+ temperature: request.temperature || 1.0,
122
+ });
123
+
124
+ for await (const event of stream) {
125
+ if (abortController.signal.aborted) break;
126
+
127
+ const eventAny = event as any;
128
+ this.logger.debug('Stream event', { type: event.type });
129
+
130
+ // 处理文本增量
131
+ if (event.type === 'content_block_delta' && eventAny.delta?.type === 'text_delta') {
132
+ const textDelta = eventAny.delta.text;
133
+ fullContent += textDelta;
134
+ subscriber.next({
135
+ type: 'text_delta',
136
+ textDelta
137
+ });
138
+ }
139
+ // 处理工具调用开始
140
+ else if (event.type === 'content_block_start' && eventAny.content_block?.type === 'tool_use') {
141
+ currentToolId = eventAny.content_block.id || `tool_${Date.now()}`;
142
+ currentToolName = eventAny.content_block.name;
143
+ currentToolInput = '';
144
+ subscriber.next({
145
+ type: 'tool_use_start',
146
+ toolCall: {
147
+ id: currentToolId,
148
+ name: currentToolName,
149
+ input: {}
150
+ }
151
+ });
152
+ this.logger.debug('Stream event', { type: 'tool_use_start', name: currentToolName });
153
+ }
154
+ // 处理工具调用参数
155
+ else if (event.type === 'content_block_delta' && eventAny.delta?.type === 'input_json_delta') {
156
+ currentToolInput += eventAny.delta.partial_json || '';
157
+ }
158
+ // 处理工具调用结束
159
+ else if (event.type === 'content_block_stop') {
160
+ if (currentToolId && currentToolName) {
161
+ let parsedInput = {};
162
+ try {
163
+ parsedInput = JSON.parse(currentToolInput || '{}');
164
+ } catch (e) {
165
+ // 使用原始输入
166
+ }
167
+ subscriber.next({
168
+ type: 'tool_use_end',
169
+ toolCall: {
170
+ id: currentToolId,
171
+ name: currentToolName,
172
+ input: parsedInput
173
+ }
174
+ });
175
+ this.logger.debug('Stream event', { type: 'tool_use_end', name: currentToolName });
176
+ currentToolId = '';
177
+ currentToolName = '';
178
+ currentToolInput = '';
179
+ }
180
+ }
181
+ }
182
+
183
+ subscriber.next({
184
+ type: 'message_end',
185
+ message: {
186
+ id: this.generateId(),
187
+ role: MessageRole.ASSISTANT,
188
+ content: fullContent,
189
+ timestamp: new Date()
190
+ }
191
+ });
192
+ this.logger.debug('Stream event', { type: 'message_end', contentLength: fullContent.length });
193
+ subscriber.complete();
194
+
195
+ } catch (error) {
196
+ if ((error as any).name !== 'AbortError') {
197
+ const errorMessage = `Anthropic stream failed: ${error instanceof Error ? error.message : String(error)}`;
198
+ this.logger.error('Stream error', error);
199
+ subscriber.next({ type: 'error', error: errorMessage });
200
+ subscriber.error(new Error(errorMessage));
201
+ }
202
+ }
203
+ };
204
+
205
+ runStream();
206
+
207
+ return () => abortController.abort();
208
+ });
101
209
  }
102
210
 
103
211
  async generateCommand(request: CommandRequest): Promise<CommandResponse> {
@@ -160,31 +268,19 @@ export class AnthropicProviderService extends BaseAiProvider {
160
268
  return this.parseAnalysisResponse(response.message.content);
161
269
  }
162
270
 
163
- async healthCheck(): Promise<HealthStatus> {
164
- try {
165
- if (!this.client) {
166
- return HealthStatus.UNHEALTHY;
167
- }
168
-
169
- const response = await this.client.messages.create({
170
- model: this.config?.model || 'claude-3-sonnet',
171
- max_tokens: 1,
172
- messages: [
173
- {
174
- role: 'user',
175
- content: 'Hi'
176
- }
177
- ]
178
- });
271
+ protected async sendTestRequest(request: ChatRequest): Promise<ChatResponse> {
272
+ if (!this.client) {
273
+ throw new Error('Anthropic client not initialized');
274
+ }
179
275
 
180
- this.lastHealthCheck = { status: HealthStatus.HEALTHY, timestamp: new Date() };
181
- return HealthStatus.HEALTHY;
276
+ const response = await this.client.messages.create({
277
+ model: this.config?.model || 'claude-3-sonnet',
278
+ max_tokens: request.maxTokens || 1,
279
+ messages: this.transformMessages(request.messages),
280
+ temperature: request.temperature || 0
281
+ });
182
282
 
183
- } catch (error) {
184
- this.logger.error('Anthropic health check failed', error);
185
- this.lastHealthCheck = { status: HealthStatus.UNHEALTHY, timestamp: new Date() };
186
- return HealthStatus.UNHEALTHY;
187
- }
283
+ return this.transformChatResponse(response);
188
284
  }
189
285
 
190
286
  validateConfig(): ValidationResult {
@@ -1,7 +1,7 @@
1
1
  import { Injectable } from '@angular/core';
2
- import { Observable } from 'rxjs';
2
+ import { Observable, of } from 'rxjs';
3
3
  import { IBaseAiProvider, ProviderConfig, AuthConfig, ProviderCapability, HealthStatus, ValidationResult, ProviderInfo, PROVIDER_DEFAULTS } from '../../types/provider.types';
4
- import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, StreamEvent } from '../../types/ai.types';
4
+ import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, StreamEvent, MessageRole } from '../../types/ai.types';
5
5
  import { LoggerService } from '../core/logger.service';
6
6
 
7
7
  /**
@@ -60,16 +60,22 @@ export abstract class BaseAiProvider implements IBaseAiProvider {
60
60
  */
61
61
  async healthCheck(): Promise<HealthStatus> {
62
62
  try {
63
- // 简单的健康检查:验证配置和连接
63
+ // 1. 验证配置
64
64
  const validation = this.validateConfig();
65
65
  if (!validation.valid) {
66
66
  this.lastHealthCheck = { status: HealthStatus.UNHEALTHY, timestamp: new Date() };
67
67
  return HealthStatus.UNHEALTHY;
68
68
  }
69
69
 
70
- // TODO: 实际的网络健康检查
71
- this.lastHealthCheck = { status: HealthStatus.HEALTHY, timestamp: new Date() };
72
- return HealthStatus.HEALTHY;
70
+ // 2. 执行网络健康检查
71
+ const networkStatus = await this.performNetworkHealthCheck();
72
+
73
+ this.lastHealthCheck = {
74
+ status: networkStatus,
75
+ timestamp: new Date()
76
+ };
77
+
78
+ return networkStatus;
73
79
 
74
80
  } catch (error) {
75
81
  this.logger.error(`Health check failed for ${this.name}`, error);
@@ -78,6 +84,64 @@ export abstract class BaseAiProvider implements IBaseAiProvider {
78
84
  }
79
85
  }
80
86
 
87
+ /**
88
+ * 执行网络健康检查 - 发送测试请求
89
+ */
90
+ protected async performNetworkHealthCheck(): Promise<HealthStatus> {
91
+ try {
92
+ // 发送一个简单的测试请求
93
+ const testRequest: ChatRequest = {
94
+ messages: [{
95
+ id: 'health-check',
96
+ role: MessageRole.USER,
97
+ content: 'Hi',
98
+ timestamp: new Date()
99
+ }],
100
+ maxTokens: 1,
101
+ temperature: 0
102
+ };
103
+
104
+ const response = await this.withRetry(() => this.sendTestRequest(testRequest));
105
+
106
+ if (this.isSuccessfulResponse(response)) {
107
+ return HealthStatus.HEALTHY;
108
+ } else {
109
+ this.logger.warn(`Health check returned unsuccessful response for ${this.name}`, {
110
+ response: this.sanitizeResponse(response)
111
+ });
112
+ return HealthStatus.DEGRADED;
113
+ }
114
+
115
+ } catch (error) {
116
+ const errorMessage = error instanceof Error ? error.message : String(error);
117
+
118
+ // 根据错误类型判断状态
119
+ if (errorMessage.includes('timeout') || errorMessage.includes('Timed out')) {
120
+ this.logger.warn(`Health check timed out for ${this.name}`);
121
+ return HealthStatus.DEGRADED;
122
+ }
123
+
124
+ if (errorMessage.includes('401') || errorMessage.includes('Unauthorized') ||
125
+ errorMessage.includes('invalid') || errorMessage.includes('API key')) {
126
+ this.logger.warn(`Health check authentication failed for ${this.name}`);
127
+ return HealthStatus.UNHEALTHY;
128
+ }
129
+
130
+ if (errorMessage.includes('429') || errorMessage.includes('rate limit')) {
131
+ this.logger.warn(`Health check rate limited for ${this.name}`);
132
+ return HealthStatus.DEGRADED;
133
+ }
134
+
135
+ this.logger.error(`Health check network error for ${this.name}`, error);
136
+ return HealthStatus.UNHEALTHY;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * 发送测试请求 - 子类必须实现
142
+ */
143
+ protected abstract sendTestRequest(request: ChatRequest): Promise<ChatResponse>;
144
+
81
145
  /**
82
146
  * 验证配置 - 默认实现,子类可以重写
83
147
  */
@@ -194,7 +258,10 @@ export abstract class BaseAiProvider implements IBaseAiProvider {
194
258
  break;
195
259
 
196
260
  case 'oauth':
197
- // TODO: 实现OAuth认证
261
+ // OAuth 认证预留
262
+ // 当前无提供商使用此认证方式
263
+ // 如需实现,可参考 OAuth 2.0 Authorization Code Flow
264
+ this.logger.debug('OAuth authentication not implemented');
198
265
  break;
199
266
  }
200
267
 
@@ -574,6 +641,22 @@ export abstract class BaseAiProvider implements IBaseAiProvider {
574
641
  5. 如果不确定当前目录或终端状态,先使用 get_terminal_cwd 或 get_terminal_list 获取信息
575
642
  6. **永远不要假装执行了操作,必须真正调用工具**
576
643
 
644
+ ## 命令执行策略
645
+ ### 快速命令(无需额外等待)
646
+ - dir, ls, cd, pwd, echo, cat, type, mkdir, rm, copy, move
647
+ - 这些命令通常在 500ms 内完成
648
+
649
+ ### 慢速命令(需要等待完整输出)
650
+ - systeminfo, ipconfig, netstat: 等待 3-8 秒
651
+ - npm, yarn, pip, docker: 等待 5-10 秒
652
+ - git: 等待 3 秒以上
653
+ - ping, tracert: 可能需要 10+ 秒
654
+
655
+ **对于慢速命令**:
656
+ 1. 执行命令后,系统会自动等待
657
+ 2. 如果输出不完整,可以再次调用 read_terminal_output 获取更新的内容
658
+ 3. **不要猜测或假设命令输出,始终以实际读取到的输出为准**
659
+
577
660
  ## 示例
578
661
  用户:"查看当前目录的文件"
579
662
  正确做法:调用 write_to_terminal 工具,参数 { "command": "dir", "execute": true }