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,748 +1,845 @@
1
- import { Injectable, NgZone } from '@angular/core';
2
- import { Subject, Observable, Subscription, BehaviorSubject, interval } from 'rxjs';
3
- import { AppService } from 'tabby-core';
4
- import { BaseTerminalTabComponent } from 'tabby-terminal';
5
- import { LoggerService } from '../core/logger.service';
6
-
7
- // 使用 any 避免泛型版本兼容问题
8
- type TerminalTab = BaseTerminalTabComponent<any>;
9
-
10
- /**
11
- * 终端信息接口
12
- */
13
- export interface TerminalInfo {
14
- id: string;
15
- title: string;
16
- isActive: boolean;
17
- cwd?: string;
18
- }
19
-
20
- /**
21
- * AI感知的终端上下文信息
22
- */
23
- export interface TerminalContext {
24
- terminalId: string;
25
- currentDirectory: string;
26
- activeShell: string;
27
- prompt: string;
28
- lastCommand?: string;
29
- processes: ProcessInfo[];
30
- environment: Record<string, string>;
31
- timestamp: number;
32
- }
33
-
34
- /**
35
- * 进程信息
36
- */
37
- export interface ProcessInfo {
38
- pid: number;
39
- name: string;
40
- command: string;
41
- status: 'running' | 'sleeping' | 'stopped';
42
- cpu?: number;
43
- memory?: number;
44
- }
45
-
46
- /**
47
- * 终端输出事件
48
- */
49
- export interface TerminalOutputEvent {
50
- terminalId: string;
51
- data: string;
52
- timestamp: number;
53
- type: 'output' | 'command' | 'error' | 'prompt';
54
- }
55
-
56
- /**
57
- * 终端管理服务
58
- * 封装 Tabby 终端 API,提供读取、写入和管理终端的能力
59
- */
60
- @Injectable({ providedIn: 'root' })
61
- export class TerminalManagerService {
62
- private outputSubscriptions = new Map<string, Subscription>();
63
- private outputSubject = new Subject<{ terminalId: string; data: string }>();
64
- private terminalChangeSubject = new Subject<void>();
65
-
66
- // AI感知相关字段
67
- private contextCache = new Map<string, TerminalContext>();
68
- private outputEventSubject = new Subject<TerminalOutputEvent>();
69
- private processMonitoringSubject = new Subject<{ terminalId: string; processes: ProcessInfo[] }>();
70
- private promptDetectionSubject = new Subject<{ terminalId: string; prompt: string }>();
71
- private monitoringIntervals = new Map<string, Subscription>();
72
-
73
- public outputEvent$ = this.outputEventSubject.asObservable();
74
- public processMonitoring$ = this.processMonitoringSubject.asObservable();
75
- public promptDetection$ = this.promptDetectionSubject.asObservable();
76
-
77
- constructor(
78
- private app: AppService,
79
- private logger: LoggerService,
80
- private zone: NgZone
81
- ) {
82
- this.logger.info('TerminalManagerService initialized');
83
-
84
- // 监听标签页变化
85
- this.app.tabsChanged$.subscribe(() => {
86
- this.terminalChangeSubject.next();
87
- });
88
- }
89
-
90
- /**
91
- * 获取当前活动终端
92
- * 注意:Tabby 将终端包装在 SplitTabComponent 中
93
- */
94
- getActiveTerminal(): TerminalTab | null {
95
- const tab = this.app.activeTab;
96
- if (!tab) return null;
97
-
98
- // 直接是终端
99
- if (this.isTerminalTab(tab)) {
100
- return tab as TerminalTab;
101
- }
102
-
103
- // SplitTabComponent 包装 - 获取聚焦的子标签页
104
- if (tab.constructor.name === 'SplitTabComponent') {
105
- const splitTab = tab as any;
106
- // 尝试获取聚焦的子标签页
107
- if (typeof splitTab.getFocusedTab === 'function') {
108
- const focusedTab = splitTab.getFocusedTab();
109
- if (focusedTab && this.isTerminalTab(focusedTab)) {
110
- return focusedTab as TerminalTab;
111
- }
112
- }
113
- // 备用:获取第一个终端
114
- if (typeof splitTab.getAllTabs === 'function') {
115
- const innerTabs = splitTab.getAllTabs() as any[];
116
- for (const innerTab of innerTabs) {
117
- if (this.isTerminalTab(innerTab)) {
118
- return innerTab as TerminalTab;
119
- }
120
- }
121
- }
122
- }
123
-
124
- return null;
125
- }
126
-
127
- /**
128
- * 获取所有终端标签
129
- * 注意:Tabby 将终端包装在 SplitTabComponent 中
130
- */
131
- getAllTerminals(): TerminalTab[] {
132
- const allTabs = this.app.tabs || [];
133
- const terminals: TerminalTab[] = [];
134
-
135
- for (const tab of allTabs) {
136
- // 如果是 SplitTabComponent,提取内部的终端
137
- if (tab.constructor.name === 'SplitTabComponent' && typeof (tab as any).getAllTabs === 'function') {
138
- const innerTabs = (tab as any).getAllTabs() as any[];
139
- for (const innerTab of innerTabs) {
140
- if (this.isTerminalTab(innerTab)) {
141
- terminals.push(innerTab as TerminalTab);
142
- }
143
- }
144
- } else if (this.isTerminalTab(tab)) {
145
- // 也检查直接的终端标签
146
- terminals.push(tab as TerminalTab);
147
- }
148
- }
149
-
150
- this.logger.info('Getting all terminals', {
151
- topLevelTabs: allTabs.length,
152
- foundTerminals: terminals.length,
153
- terminalTitles: terminals.map(t => t.title)
154
- });
155
-
156
- return terminals;
157
- }
158
-
159
- /**
160
- * 获取所有终端信息
161
- */
162
- getAllTerminalInfo(): TerminalInfo[] {
163
- const activeTerminal = this.getActiveTerminal();
164
- return this.getAllTerminals().map((terminal, index) => ({
165
- id: `terminal-${index}`,
166
- title: terminal.title || `Terminal ${index + 1}`,
167
- isActive: terminal === activeTerminal,
168
- cwd: this.getTerminalCwd(terminal)
169
- }));
170
- }
171
-
172
- /**
173
- * 向当前终端发送命令
174
- */
175
- sendCommand(command: string, execute: boolean = true): boolean {
176
- const terminal = this.getActiveTerminal();
177
- if (!terminal) {
178
- this.logger.warn('No active terminal found');
179
- return false;
180
- }
181
-
182
- return this.sendCommandToTerminal(terminal, command, execute);
183
- }
184
-
185
- /**
186
- * 向指定终端发送命令
187
- */
188
- sendCommandToTerminal(terminal: TerminalTab, command: string, execute: boolean = true): boolean {
189
- try {
190
- const fullCommand = execute ? command + '\r' : command;
191
-
192
- // 调试:检查终端对象状态
193
- this.logger.info('Terminal object details', {
194
- hasSession: !!(terminal as any).session,
195
- hasFrontend: !!(terminal as any).frontend,
196
- hasSendInput: typeof terminal.sendInput === 'function',
197
- terminalTitle: terminal.title
198
- });
199
-
200
- // 优先使用 sendInput(标准 API)
201
- if (typeof terminal.sendInput === 'function') {
202
- this.logger.info('Using terminal.sendInput');
203
- terminal.sendInput(fullCommand);
204
- this.logger.info('Command sent via sendInput', { command, execute });
205
- return true;
206
- }
207
-
208
- // 备用:使用 session.write
209
- const session = (terminal as any).session;
210
- if (session && typeof session.write === 'function') {
211
- this.logger.info('Using session.write');
212
- session.write(fullCommand);
213
- this.logger.info('Command sent via session.write', { command, execute });
214
- return true;
215
- }
216
-
217
- this.logger.warn('No valid method to send command found');
218
- return false;
219
- } catch (error) {
220
- this.logger.error('Failed to send command to terminal', error);
221
- return false;
222
- }
223
- }
224
-
225
- /**
226
- * 向指定索引的终端发送命令
227
- */
228
- sendCommandToIndex(index: number, command: string, execute: boolean = true): boolean {
229
- const terminals = this.getAllTerminals();
230
- if (index < 0 || index >= terminals.length) {
231
- this.logger.warn('Invalid terminal index', { index, count: terminals.length });
232
- return false;
233
- }
234
-
235
- return this.sendCommandToTerminal(terminals[index], command, execute);
236
- }
237
-
238
- /**
239
- * 获取当前终端的选中文本
240
- */
241
- getSelection(): string {
242
- const terminal = this.getActiveTerminal();
243
- if (!terminal || !terminal.frontend) {
244
- return '';
245
- }
246
-
247
- try {
248
- // Tabby 使用 frontend.getSelection() 获取选中内容
249
- const selection = terminal.frontend.getSelection?.();
250
- return selection || '';
251
- } catch (error) {
252
- this.logger.error('Failed to get selection', error);
253
- return '';
254
- }
255
- }
256
-
257
- /**
258
- * 获取终端工作目录
259
- */
260
- getTerminalCwd(terminal?: TerminalTab): string | undefined {
261
- const t = terminal || this.getActiveTerminal();
262
- if (!t) return undefined;
263
-
264
- try {
265
- // 尝试从会话获取 cwd
266
- const session = (t as any).session;
267
- if (session?.cwd) {
268
- return session.cwd;
269
- }
270
- return undefined;
271
- } catch (error) {
272
- return undefined;
273
- }
274
- }
275
-
276
- /**
277
- * 切换到指定终端
278
- * 修复:需要选择顶层 Tab (SplitTabComponent),而非内部终端
279
- */
280
- focusTerminal(index: number): boolean {
281
- const allTabs = this.app.tabs || [];
282
- const terminals = this.getAllTerminals();
283
-
284
- if (index < 0 || index >= terminals.length) {
285
- this.logger.warn('Invalid terminal index', { index, count: terminals.length });
286
- return false;
287
- }
288
-
289
- const targetTerminal = terminals[index];
290
-
291
- try {
292
- // 步骤1:查找包含目标终端的顶层 Tab
293
- let topLevelTab: any = null;
294
- let splitTabRef: any = null;
295
-
296
- for (const tab of allTabs) {
297
- // 情况1:直接是终端 Tab(不在 SplitTabComponent 内)
298
- if (this.isTerminalTab(tab) && tab === targetTerminal) {
299
- topLevelTab = tab;
300
- break;
301
- }
302
-
303
- // 情况2:终端在 SplitTabComponent 内部
304
- if (tab.constructor.name === 'SplitTabComponent') {
305
- const splitTab = tab as any;
306
- if (typeof splitTab.getAllTabs === 'function') {
307
- const innerTabs = splitTab.getAllTabs() as any[];
308
- if (innerTabs.includes(targetTerminal)) {
309
- topLevelTab = tab; // 顶层 SplitTabComponent
310
- splitTabRef = splitTab; // 保存引用,用于内部聚焦
311
- break;
312
- }
313
- }
314
- }
315
- }
316
-
317
- if (!topLevelTab) {
318
- this.logger.warn('Target terminal not found', { index });
319
- return false;
320
- }
321
-
322
- // 步骤2:在 Angular Zone 内执行 UI 变更,确保触发变更检测
323
- this.zone.run(() => {
324
- // 1. 选择顶层 Tab(如果不是当前的,避免重复调用 selectTab)
325
- if (this.app.activeTab !== topLevelTab) {
326
- this.app.selectTab(topLevelTab);
327
- }
328
-
329
- // 2. 关键修复:调用 SplitTabComponent.focus() 切换内部焦点
330
- if (splitTabRef && typeof splitTabRef.focus === 'function') {
331
- splitTabRef.focus(targetTerminal);
332
- }
333
-
334
- // 3. 确保终端获得输入焦点
335
- const terminalAny = targetTerminal as any;
336
- if (typeof terminalAny.focus === 'function') {
337
- terminalAny.focus();
338
- }
339
- });
340
-
341
- this.logger.info('Focused terminal', {
342
- index,
343
- title: targetTerminal.title,
344
- isInSplitTab: !!splitTabRef
345
- });
346
-
347
- return true;
348
- } catch (error) {
349
- this.logger.error('Failed to focus terminal', error);
350
- return false;
351
- }
352
- }
353
-
354
- /**
355
- * 订阅当前终端输出
356
- */
357
- subscribeToActiveTerminalOutput(callback: (data: string) => void): Subscription | null {
358
- const terminal = this.getActiveTerminal();
359
- if (!terminal) {
360
- this.logger.warn('No active terminal to subscribe');
361
- return null;
362
- }
363
-
364
- return this.subscribeToTerminalOutput(terminal, callback);
365
- }
366
-
367
- /**
368
- * 订阅指定终端输出
369
- */
370
- subscribeToTerminalOutput(terminal: TerminalTab, callback: (data: string) => void): Subscription {
371
- return terminal.output$.subscribe(data => {
372
- callback(data);
373
- });
374
- }
375
-
376
- /**
377
- * 获取终端变化事件流
378
- */
379
- onTerminalChange(): Observable<void> {
380
- return this.terminalChangeSubject.asObservable();
381
- }
382
-
383
- /**
384
- * 获取终端数量
385
- */
386
- getTerminalCount(): number {
387
- return this.getAllTerminals().length;
388
- }
389
-
390
- /**
391
- * 检查是否有可用终端
392
- */
393
- hasTerminal(): boolean {
394
- return this.getTerminalCount() > 0;
395
- }
396
-
397
- /**
398
- * 检查标签页是否是终端
399
- * 使用特征检测避免 instanceof 在 webpack 打包后失效
400
- */
401
- private isTerminalTab(tab: any): boolean {
402
- if (!tab) return false;
403
-
404
- // 方法1: instanceof 检测
405
- if (tab instanceof BaseTerminalTabComponent) {
406
- this.logger.debug('Terminal detected via instanceof');
407
- return true;
408
- }
409
-
410
- // 方法2: 使用 in 操作符检测 sendInput(包括原型链)
411
- if ('sendInput' in tab && 'frontend' in tab) {
412
- this.logger.debug('Terminal detected via sendInput in proto');
413
- return true;
414
- }
415
-
416
- // 方法3: 检测 session 和 frontend 属性
417
- if (tab.session !== undefined && tab.frontend !== undefined) {
418
- this.logger.debug('Terminal detected via session+frontend');
419
- return true;
420
- }
421
-
422
- // 方法4: 检查原型链名称
423
- let proto = Object.getPrototypeOf(tab);
424
- while (proto && proto.constructor) {
425
- const protoName = proto.constructor.name || '';
426
- if (protoName.includes('Terminal') || protoName.includes('SSH') ||
427
- protoName.includes('Local') || protoName.includes('Telnet') ||
428
- protoName.includes('Serial') || protoName.includes('BaseTerminal')) {
429
- this.logger.debug('Terminal detected via prototype:', protoName);
430
- return true;
431
- }
432
- if (proto === Object.prototype) break;
433
- proto = Object.getPrototypeOf(proto);
434
- }
435
-
436
- return false;
437
- }
438
-
439
- /**
440
- * 清理资源
441
- */
442
- dispose(): void {
443
- this.outputSubscriptions.forEach(sub => sub.unsubscribe());
444
- this.outputSubscriptions.clear();
445
- this.monitoringIntervals.forEach(sub => sub.unsubscribe());
446
- this.monitoringIntervals.clear();
447
- }
448
-
449
- // ==================== AI感知能力 ====================
450
-
451
- /**
452
- * 检测当前目录
453
- */
454
- async detectCurrentDirectory(terminal?: TerminalTab): Promise<string> {
455
- const t = terminal || this.getActiveTerminal();
456
- if (!t) {
457
- return process.cwd();
458
- }
459
-
460
- try {
461
- // 首先尝试从会话获取
462
- const cwd = this.getTerminalCwd(t);
463
- if (cwd) {
464
- return cwd;
465
- }
466
-
467
- // 如果无法从会话获取,使用 pwd 命令
468
- const originalPrompt = await this.getPrompt(t);
469
- this.sendCommandToTerminal(t, 'pwd', true);
470
-
471
- // 等待输出(简化实现)
472
- await new Promise(resolve => setTimeout(resolve, 500));
473
-
474
- const newPrompt = await this.getPrompt(t);
475
- const pwdOutput = this.extractCommandOutput(originalPrompt, newPrompt, 'pwd');
476
-
477
- return pwdOutput || process.cwd();
478
- } catch (error) {
479
- this.logger.error('Failed to detect current directory', error);
480
- return process.cwd();
481
- }
482
- }
483
-
484
- /**
485
- * 获取活跃Shell
486
- */
487
- async getActiveShell(terminal?: TerminalTab): Promise<string> {
488
- const t = terminal || this.getActiveTerminal();
489
- if (!t) {
490
- return 'unknown';
491
- }
492
-
493
- try {
494
- // 尝试从环境变量获取
495
- const shell = process.env.SHELL || process.env.COMSPEC || 'unknown';
496
-
497
- // 如果无法从环境变量获取,使用 echo $SHELL (Unix) echo %COMSPEC% (Windows)
498
- const shellCommand = process.platform === 'win32' ? 'echo %COMSPEC%' : 'echo $SHELL';
499
- this.sendCommandToTerminal(t, shellCommand, true);
500
-
501
- await new Promise(resolve => setTimeout(resolve, 500));
502
-
503
- return shell;
504
- } catch (error) {
505
- this.logger.error('Failed to detect active shell', error);
506
- return 'unknown';
507
- }
508
- }
509
-
510
- /**
511
- * 监控输出流
512
- */
513
- monitorOutput(terminal?: TerminalTab): Observable<TerminalOutputEvent> {
514
- const t = terminal || this.getActiveTerminal();
515
- if (!t) {
516
- throw new Error('No terminal available for monitoring');
517
- }
518
-
519
- const terminalId = this.getTerminalId(t);
520
-
521
- // 订阅终端输出
522
- const subscription = t.output$.subscribe(data => {
523
- const event: TerminalOutputEvent = {
524
- terminalId,
525
- data,
526
- timestamp: Date.now(),
527
- type: this.detectOutputType(data)
528
- };
529
-
530
- this.outputEventSubject.next(event);
531
- });
532
-
533
- this.monitoringIntervals.set(terminalId, subscription);
534
-
535
- return this.outputEvent$.pipe(
536
- // 只返回当前终端的事件
537
- // 注意:实际实现中需要过滤
538
- );
539
- }
540
-
541
- /**
542
- * 检测提示符
543
- */
544
- async getPrompt(terminal?: TerminalTab): Promise<string> {
545
- const t = terminal || this.getActiveTerminal();
546
- if (!t) {
547
- return '';
548
- }
549
-
550
- try {
551
- // 发送一个空命令来触发提示符显示
552
- this.sendCommandToTerminal(t, '', true);
553
- await new Promise(resolve => setTimeout(resolve, 200));
554
-
555
- // 简化实现:返回默认提示符格式
556
- const shell = await this.getActiveShell(t);
557
- const cwd = await this.detectCurrentDirectory(t);
558
-
559
- return this.formatPrompt(shell, cwd);
560
- } catch (error) {
561
- this.logger.error('Failed to detect prompt', error);
562
- return '$ ';
563
- }
564
- }
565
-
566
- /**
567
- * 追踪进程
568
- */
569
- async trackProcesses(terminal?: TerminalTab): Promise<ProcessInfo[]> {
570
- const t = terminal || this.getActiveTerminal();
571
- if (!t) {
572
- return [];
573
- }
574
-
575
- try {
576
- // 使用 ps 命令获取进程列表
577
- const psCommand = process.platform === 'win32' ? 'tasklist' : 'ps aux';
578
- this.sendCommandToTerminal(t, psCommand, true);
579
-
580
- await new Promise(resolve => setTimeout(resolve, 1000));
581
-
582
- // 简化实现:返回模拟进程信息
583
- const processes: ProcessInfo[] = [
584
- {
585
- pid: process.pid,
586
- name: process.platform === 'win32' ? 'node.exe' : 'node',
587
- command: process.argv0,
588
- status: 'running'
589
- }
590
- ];
591
-
592
- this.processMonitoringSubject.next({
593
- terminalId: this.getTerminalId(t),
594
- processes
595
- });
596
-
597
- return processes;
598
- } catch (error) {
599
- this.logger.error('Failed to track processes', error);
600
- return [];
601
- }
602
- }
603
-
604
- /**
605
- * 获取终端AI上下文
606
- */
607
- async getTerminalContext(terminal?: TerminalTab): Promise<TerminalContext> {
608
- const t = terminal || this.getActiveTerminal();
609
- if (!t) {
610
- throw new Error('No terminal available');
611
- }
612
-
613
- const terminalId = this.getTerminalId(t);
614
-
615
- // 检查缓存
616
- if (this.contextCache.has(terminalId)) {
617
- const cached = this.contextCache.get(terminalId)!;
618
- // 如果缓存不超过5秒,直接返回
619
- if (Date.now() - cached.timestamp < 5000) {
620
- return cached;
621
- }
622
- }
623
-
624
- // 获取最新的上下文信息
625
- const [currentDirectory, activeShell, prompt, processes] = await Promise.all([
626
- this.detectCurrentDirectory(t),
627
- this.getActiveShell(t),
628
- this.getPrompt(t),
629
- this.trackProcesses(t)
630
- ]);
631
-
632
- const context: TerminalContext = {
633
- terminalId,
634
- currentDirectory,
635
- activeShell,
636
- prompt,
637
- processes,
638
- environment: this.filterEnvVariables(process.env),
639
- timestamp: Date.now()
640
- };
641
-
642
- // 更新缓存
643
- this.contextCache.set(terminalId, context);
644
-
645
- return context;
646
- }
647
-
648
- /**
649
- * 清理终端上下文缓存
650
- */
651
- clearContextCache(terminalId?: string): void {
652
- if (terminalId) {
653
- this.contextCache.delete(terminalId);
654
- } else {
655
- this.contextCache.clear();
656
- }
657
- }
658
-
659
- /**
660
- * 开始持续监控终端
661
- */
662
- startContinuousMonitoring(intervalMs: number = 5000): void {
663
- const terminals = this.getAllTerminals();
664
-
665
- terminals.forEach(terminal => {
666
- const terminalId = this.getTerminalId(terminal);
667
-
668
- // 如果已在监控,先停止
669
- if (this.monitoringIntervals.has(terminalId)) {
670
- return;
671
- }
672
-
673
- const subscription = interval(intervalMs).subscribe(async () => {
674
- try {
675
- await this.getTerminalContext(terminal);
676
- } catch (error) {
677
- this.logger.error('Failed to monitor terminal', { terminalId, error });
678
- }
679
- });
680
-
681
- this.monitoringIntervals.set(terminalId, subscription);
682
- });
683
-
684
- this.logger.info('Started continuous monitoring', {
685
- terminalCount: terminals.length,
686
- intervalMs
687
- });
688
- }
689
-
690
- /**
691
- * 停止持续监控
692
- */
693
- stopContinuousMonitoring(terminalId?: string): void {
694
- if (terminalId) {
695
- const subscription = this.monitoringIntervals.get(terminalId);
696
- if (subscription) {
697
- subscription.unsubscribe();
698
- this.monitoringIntervals.delete(terminalId);
699
- }
700
- } else {
701
- this.monitoringIntervals.forEach(sub => sub.unsubscribe());
702
- this.monitoringIntervals.clear();
703
- }
704
-
705
- this.logger.info('Stopped continuous monitoring', { terminalId: terminalId || 'all' });
706
- }
707
-
708
- // ==================== 私有辅助方法 ====================
709
-
710
- private getTerminalId(terminal: TerminalTab): string {
711
- return terminal.title || `terminal-${Math.random().toString(36).substr(2, 9)}`;
712
- }
713
-
714
- private detectOutputType(data: string): 'output' | 'command' | 'error' | 'prompt' {
715
- if (data.includes('error') || data.includes('Error') || data.includes('ERROR')) {
716
- return 'error';
717
- }
718
- if (data.includes('$') || data.includes('#') || data.includes('>')) {
719
- return 'prompt';
720
- }
721
- if (data.includes('\r\n') || data.includes('\n')) {
722
- return 'output';
723
- }
724
- return 'command';
725
- }
726
-
727
- private formatPrompt(shell: string, cwd: string): string {
728
- const shellName = shell.split('/').pop() || shell;
729
- return `${shellName}:${cwd}$ `;
730
- }
731
-
732
- private extractCommandOutput(originalPrompt: string, newPrompt: string, command: string): string {
733
- // 简化实现:实际应该解析终端输出
734
- const lines = newPrompt.split('\n');
735
- return lines.find(line => line.includes(command) && !line.includes('$')) || '';
736
- }
737
-
738
- private filterEnvVariables(env: NodeJS.ProcessEnv): Record<string, string> {
739
- const result: Record<string, string> = {};
740
- for (const key of Object.keys(env)) {
741
- const value = env[key];
742
- if (value !== undefined) {
743
- result[key] = value;
744
- }
745
- }
746
- return result;
747
- }
748
- }
1
+ import { Injectable, NgZone } from '@angular/core';
2
+ import { Subject, Observable, Subscription, BehaviorSubject, interval } from 'rxjs';
3
+ import { AppService } from 'tabby-core';
4
+ import { BaseTerminalTabComponent } from 'tabby-terminal';
5
+ import { LoggerService } from '../core/logger.service';
6
+
7
+ // 使用 any 避免泛型版本兼容问题
8
+ type TerminalTab = BaseTerminalTabComponent<any>;
9
+
10
+ /**
11
+ * 终端信息接口
12
+ */
13
+ export interface TerminalInfo {
14
+ id: string;
15
+ title: string;
16
+ isActive: boolean;
17
+ cwd?: string;
18
+ }
19
+
20
+ /**
21
+ * AI感知的终端上下文信息
22
+ */
23
+ export interface TerminalContext {
24
+ terminalId: string;
25
+ currentDirectory: string;
26
+ activeShell: string;
27
+ prompt: string;
28
+ lastCommand?: string;
29
+ processes: ProcessInfo[];
30
+ environment: Record<string, string>;
31
+ timestamp: number;
32
+ }
33
+
34
+ /**
35
+ * 进程信息
36
+ */
37
+ export interface ProcessInfo {
38
+ pid: number;
39
+ name: string;
40
+ command: string;
41
+ status: 'running' | 'sleeping' | 'stopped';
42
+ cpu?: number;
43
+ memory?: number;
44
+ }
45
+
46
+ /**
47
+ * 终端输出事件
48
+ */
49
+ export interface TerminalOutputEvent {
50
+ terminalId: string;
51
+ data: string;
52
+ timestamp: number;
53
+ type: 'output' | 'command' | 'error' | 'prompt';
54
+ }
55
+
56
+ /**
57
+ * 终端管理服务
58
+ * 封装 Tabby 终端 API,提供读取、写入和管理终端的能力
59
+ */
60
+ @Injectable({ providedIn: 'root' })
61
+ export class TerminalManagerService {
62
+ private outputSubscriptions = new Map<string, Subscription>();
63
+ private outputSubject = new Subject<{ terminalId: string; data: string }>();
64
+ private terminalChangeSubject = new Subject<void>();
65
+
66
+ // AI感知相关字段
67
+ private contextCache = new Map<string, TerminalContext>();
68
+ private outputEventSubject = new Subject<TerminalOutputEvent>();
69
+ private processMonitoringSubject = new Subject<{ terminalId: string; processes: ProcessInfo[] }>();
70
+ private promptDetectionSubject = new Subject<{ terminalId: string; prompt: string }>();
71
+ private monitoringIntervals = new Map<string, Subscription>();
72
+
73
+ public outputEvent$ = this.outputEventSubject.asObservable();
74
+ public processMonitoring$ = this.processMonitoringSubject.asObservable();
75
+ public promptDetection$ = this.promptDetectionSubject.asObservable();
76
+
77
+ constructor(
78
+ private app: AppService,
79
+ private logger: LoggerService,
80
+ private zone: NgZone
81
+ ) {
82
+ this.logger.info('TerminalManagerService initialized');
83
+
84
+ // 监听标签页变化
85
+ this.app.tabsChanged$.subscribe(() => {
86
+ this.terminalChangeSubject.next();
87
+ });
88
+ }
89
+
90
+ /**
91
+ * 获取当前活动终端
92
+ * 注意:Tabby 将终端包装在 SplitTabComponent 中
93
+ */
94
+ getActiveTerminal(): TerminalTab | null {
95
+ const tab = this.app.activeTab;
96
+ if (!tab) return null;
97
+
98
+ // 直接是终端
99
+ if (this.isTerminalTab(tab)) {
100
+ return tab as TerminalTab;
101
+ }
102
+
103
+ // SplitTabComponent 包装 - 获取聚焦的子标签页
104
+ if (tab.constructor.name === 'SplitTabComponent') {
105
+ const splitTab = tab as any;
106
+ // 尝试获取聚焦的子标签页
107
+ if (typeof splitTab.getFocusedTab === 'function') {
108
+ const focusedTab = splitTab.getFocusedTab();
109
+ if (focusedTab && this.isTerminalTab(focusedTab)) {
110
+ return focusedTab as TerminalTab;
111
+ }
112
+ }
113
+ // 备用:获取第一个终端
114
+ if (typeof splitTab.getAllTabs === 'function') {
115
+ const innerTabs = splitTab.getAllTabs() as any[];
116
+ for (const innerTab of innerTabs) {
117
+ if (this.isTerminalTab(innerTab)) {
118
+ return innerTab as TerminalTab;
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ return null;
125
+ }
126
+
127
+ /**
128
+ * 获取所有终端标签
129
+ * 注意:Tabby 将终端包装在 SplitTabComponent 中
130
+ */
131
+ getAllTerminals(): TerminalTab[] {
132
+ const allTabs = this.app.tabs || [];
133
+ const terminals: TerminalTab[] = [];
134
+
135
+ for (const tab of allTabs) {
136
+ // 如果是 SplitTabComponent,提取内部的终端
137
+ if (tab.constructor.name === 'SplitTabComponent' && typeof (tab as any).getAllTabs === 'function') {
138
+ const innerTabs = (tab as any).getAllTabs() as any[];
139
+ for (const innerTab of innerTabs) {
140
+ if (this.isTerminalTab(innerTab)) {
141
+ terminals.push(innerTab as TerminalTab);
142
+ }
143
+ }
144
+ } else if (this.isTerminalTab(tab)) {
145
+ // 也检查直接的终端标签
146
+ terminals.push(tab as TerminalTab);
147
+ }
148
+ }
149
+
150
+ this.logger.info('Getting all terminals', {
151
+ topLevelTabs: allTabs.length,
152
+ foundTerminals: terminals.length,
153
+ terminalTitles: terminals.map(t => t.title)
154
+ });
155
+
156
+ return terminals;
157
+ }
158
+
159
+ /**
160
+ * 获取所有终端信息
161
+ */
162
+ getAllTerminalInfo(): TerminalInfo[] {
163
+ const activeTerminal = this.getActiveTerminal();
164
+ return this.getAllTerminals().map((terminal, index) => ({
165
+ id: `terminal-${index}`,
166
+ title: terminal.title || `Terminal ${index + 1}`,
167
+ isActive: terminal === activeTerminal,
168
+ cwd: this.getTerminalCwd(terminal)
169
+ }));
170
+ }
171
+
172
+ /**
173
+ * 向当前终端发送命令
174
+ */
175
+ sendCommand(command: string, execute: boolean = true): boolean {
176
+ const terminal = this.getActiveTerminal();
177
+ if (!terminal) {
178
+ this.logger.warn('No active terminal found');
179
+ return false;
180
+ }
181
+
182
+ return this.sendCommandToTerminal(terminal, command, execute);
183
+ }
184
+
185
+ /**
186
+ * 向指定终端发送命令
187
+ */
188
+ sendCommandToTerminal(terminal: TerminalTab, command: string, execute: boolean = true): boolean {
189
+ try {
190
+ const fullCommand = execute ? command + '\r' : command;
191
+
192
+ // 调试:检查终端对象状态
193
+ this.logger.info('Terminal object details', {
194
+ hasSession: !!(terminal as any).session,
195
+ hasFrontend: !!(terminal as any).frontend,
196
+ hasSendInput: typeof terminal.sendInput === 'function',
197
+ terminalTitle: terminal.title
198
+ });
199
+
200
+ // 优先使用 sendInput(标准 API)
201
+ if (typeof terminal.sendInput === 'function') {
202
+ this.logger.info('Using terminal.sendInput');
203
+ terminal.sendInput(fullCommand);
204
+ this.logger.info('Command sent via sendInput', { command, execute });
205
+ return true;
206
+ }
207
+
208
+ // 备用:使用 session.write
209
+ const session = (terminal as any).session;
210
+ if (session && typeof session.write === 'function') {
211
+ this.logger.info('Using session.write');
212
+ session.write(fullCommand);
213
+ this.logger.info('Command sent via session.write', { command, execute });
214
+ return true;
215
+ }
216
+
217
+ this.logger.warn('No valid method to send command found');
218
+ return false;
219
+ } catch (error) {
220
+ this.logger.error('Failed to send command to terminal', error);
221
+ return false;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * 向指定索引的终端发送命令
227
+ */
228
+ sendCommandToIndex(index: number, command: string, execute: boolean = true): boolean {
229
+ const terminals = this.getAllTerminals();
230
+ if (index < 0 || index >= terminals.length) {
231
+ this.logger.warn('Invalid terminal index', { index, count: terminals.length });
232
+ return false;
233
+ }
234
+
235
+ return this.sendCommandToTerminal(terminals[index], command, execute);
236
+ }
237
+
238
+ /**
239
+ * 获取当前终端的选中文本
240
+ */
241
+ getSelection(): string {
242
+ const terminal = this.getActiveTerminal();
243
+ if (!terminal || !terminal.frontend) {
244
+ return '';
245
+ }
246
+
247
+ try {
248
+ // Tabby 使用 frontend.getSelection() 获取选中内容
249
+ const selection = terminal.frontend.getSelection?.();
250
+ return selection || '';
251
+ } catch (error) {
252
+ this.logger.error('Failed to get selection', error);
253
+ return '';
254
+ }
255
+ }
256
+
257
+ /**
258
+ * 获取当前终端选中的文本(别名)
259
+ * 用于快捷键功能
260
+ */
261
+ getSelectedText(): string | null {
262
+ const selection = this.getSelection();
263
+ return selection || null;
264
+ }
265
+
266
+ /**
267
+ * 获取当前终端最后一条命令
268
+ * 用于快捷键功能 - 命令解释
269
+ */
270
+ getLastCommand(): string | null {
271
+ const output = this.readTerminalOutput(10);
272
+ if (!output) return null;
273
+
274
+ // 尝试提取最后一条命令(通常是 $ 或 > 后面的内容)
275
+ const lines = output.split('\n').filter(l => l.trim());
276
+
277
+ for (let i = lines.length - 1; i >= 0; i--) {
278
+ const line = lines[i];
279
+ // 匹配常见的命令提示符
280
+ const match = line.match(/(?:[$>%#]|PS [^>]+>)\s*(.+)/);
281
+ if (match && match[1]) {
282
+ const cmd = match[1].trim();
283
+ // 排除明显的非命令行
284
+ if (cmd && !cmd.startsWith('[') && cmd.length > 0) {
285
+ return cmd;
286
+ }
287
+ }
288
+ }
289
+ return null;
290
+ }
291
+
292
+ /**
293
+ * 获取当前终端最近的输出(用于解释)
294
+ * 用于快捷键功能 - 获取终端上下文
295
+ */
296
+ getRecentContext(): string {
297
+ return this.readTerminalOutput(20) || '';
298
+ }
299
+
300
+ /**
301
+ * 获取终端工作目录
302
+ */
303
+ getTerminalCwd(terminal?: TerminalTab): string | undefined {
304
+ const t = terminal || this.getActiveTerminal();
305
+ if (!t) return undefined;
306
+
307
+ try {
308
+ // 尝试从会话获取 cwd
309
+ const session = (t as any).session;
310
+ if (session?.cwd) {
311
+ return session.cwd;
312
+ }
313
+ return undefined;
314
+ } catch (error) {
315
+ return undefined;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * 切换到指定终端
321
+ * 修复:需要选择顶层 Tab (SplitTabComponent),而非内部终端
322
+ */
323
+ focusTerminal(index: number): boolean {
324
+ const allTabs = this.app.tabs || [];
325
+ const terminals = this.getAllTerminals();
326
+
327
+ if (index < 0 || index >= terminals.length) {
328
+ this.logger.warn('Invalid terminal index', { index, count: terminals.length });
329
+ return false;
330
+ }
331
+
332
+ const targetTerminal = terminals[index];
333
+
334
+ try {
335
+ // 步骤1:查找包含目标终端的顶层 Tab
336
+ let topLevelTab: any = null;
337
+ let splitTabRef: any = null;
338
+
339
+ for (const tab of allTabs) {
340
+ // 情况1:直接是终端 Tab(不在 SplitTabComponent 内)
341
+ if (this.isTerminalTab(tab) && tab === targetTerminal) {
342
+ topLevelTab = tab;
343
+ break;
344
+ }
345
+
346
+ // 情况2:终端在 SplitTabComponent 内部
347
+ if (tab.constructor.name === 'SplitTabComponent') {
348
+ const splitTab = tab as any;
349
+ if (typeof splitTab.getAllTabs === 'function') {
350
+ const innerTabs = splitTab.getAllTabs() as any[];
351
+ if (innerTabs.includes(targetTerminal)) {
352
+ topLevelTab = tab; // 顶层 SplitTabComponent
353
+ splitTabRef = splitTab; // 保存引用,用于内部聚焦
354
+ break;
355
+ }
356
+ }
357
+ }
358
+ }
359
+
360
+ if (!topLevelTab) {
361
+ this.logger.warn('Target terminal not found', { index });
362
+ return false;
363
+ }
364
+
365
+ // 步骤2:在 Angular Zone 内执行 UI 变更,确保触发变更检测
366
+ this.zone.run(() => {
367
+ // 1. 选择顶层 Tab(如果不是当前的,避免重复调用 selectTab)
368
+ if (this.app.activeTab !== topLevelTab) {
369
+ this.app.selectTab(topLevelTab);
370
+ }
371
+
372
+ // 2. 关键修复:调用 SplitTabComponent.focus() 切换内部焦点
373
+ if (splitTabRef && typeof splitTabRef.focus === 'function') {
374
+ splitTabRef.focus(targetTerminal);
375
+ }
376
+
377
+ // 3. 确保终端获得输入焦点
378
+ const terminalAny = targetTerminal as any;
379
+ if (typeof terminalAny.focus === 'function') {
380
+ terminalAny.focus();
381
+ }
382
+ });
383
+
384
+ this.logger.info('Focused terminal', {
385
+ index,
386
+ title: targetTerminal.title,
387
+ isInSplitTab: !!splitTabRef
388
+ });
389
+
390
+ return true;
391
+ } catch (error) {
392
+ this.logger.error('Failed to focus terminal', error);
393
+ return false;
394
+ }
395
+ }
396
+
397
+ /**
398
+ * 订阅当前终端输出
399
+ */
400
+ subscribeToActiveTerminalOutput(callback: (data: string) => void): Subscription | null {
401
+ const terminal = this.getActiveTerminal();
402
+ if (!terminal) {
403
+ this.logger.warn('No active terminal to subscribe');
404
+ return null;
405
+ }
406
+
407
+ return this.subscribeToTerminalOutput(terminal, callback);
408
+ }
409
+
410
+ /**
411
+ * 订阅指定终端输出
412
+ */
413
+ subscribeToTerminalOutput(terminal: TerminalTab, callback: (data: string) => void): Subscription {
414
+ return terminal.output$.subscribe(data => {
415
+ callback(data);
416
+ });
417
+ }
418
+
419
+ /**
420
+ * 获取终端变化事件流
421
+ */
422
+ onTerminalChange(): Observable<void> {
423
+ return this.terminalChangeSubject.asObservable();
424
+ }
425
+
426
+ /**
427
+ * 获取终端数量
428
+ */
429
+ getTerminalCount(): number {
430
+ return this.getAllTerminals().length;
431
+ }
432
+
433
+ /**
434
+ * 检查是否有可用终端
435
+ */
436
+ hasTerminal(): boolean {
437
+ return this.getTerminalCount() > 0;
438
+ }
439
+
440
+ /**
441
+ * 检查标签页是否是终端
442
+ * 使用特征检测避免 instanceof 在 webpack 打包后失效
443
+ */
444
+ private isTerminalTab(tab: any): boolean {
445
+ if (!tab) return false;
446
+
447
+ // 方法1: instanceof 检测
448
+ if (tab instanceof BaseTerminalTabComponent) {
449
+ this.logger.debug('Terminal detected via instanceof');
450
+ return true;
451
+ }
452
+
453
+ // 方法2: 使用 in 操作符检测 sendInput(包括原型链)
454
+ if ('sendInput' in tab && 'frontend' in tab) {
455
+ this.logger.debug('Terminal detected via sendInput in proto');
456
+ return true;
457
+ }
458
+
459
+ // 方法3: 检测 session 和 frontend 属性
460
+ if (tab.session !== undefined && tab.frontend !== undefined) {
461
+ this.logger.debug('Terminal detected via session+frontend');
462
+ return true;
463
+ }
464
+
465
+ // 方法4: 检查原型链名称
466
+ let proto = Object.getPrototypeOf(tab);
467
+ while (proto && proto.constructor) {
468
+ const protoName = proto.constructor.name || '';
469
+ if (protoName.includes('Terminal') || protoName.includes('SSH') ||
470
+ protoName.includes('Local') || protoName.includes('Telnet') ||
471
+ protoName.includes('Serial') || protoName.includes('BaseTerminal')) {
472
+ this.logger.debug('Terminal detected via prototype:', protoName);
473
+ return true;
474
+ }
475
+ if (proto === Object.prototype) break;
476
+ proto = Object.getPrototypeOf(proto);
477
+ }
478
+
479
+ return false;
480
+ }
481
+
482
+ /**
483
+ * 清理资源
484
+ */
485
+ dispose(): void {
486
+ this.outputSubscriptions.forEach(sub => sub.unsubscribe());
487
+ this.outputSubscriptions.clear();
488
+ this.monitoringIntervals.forEach(sub => sub.unsubscribe());
489
+ this.monitoringIntervals.clear();
490
+ }
491
+
492
+ // ==================== AI感知能力 ====================
493
+
494
+ /**
495
+ * 检测当前目录
496
+ */
497
+ async detectCurrentDirectory(terminal?: TerminalTab): Promise<string> {
498
+ const t = terminal || this.getActiveTerminal();
499
+ if (!t) {
500
+ return process.cwd();
501
+ }
502
+
503
+ try {
504
+ // 首先尝试从会话获取
505
+ const cwd = this.getTerminalCwd(t);
506
+ if (cwd) {
507
+ return cwd;
508
+ }
509
+
510
+ // 如果无法从会话获取,使用 pwd 命令
511
+ const originalPrompt = await this.getPrompt(t);
512
+ this.sendCommandToTerminal(t, 'pwd', true);
513
+
514
+ // 等待输出(简化实现)
515
+ await new Promise(resolve => setTimeout(resolve, 500));
516
+
517
+ const newPrompt = await this.getPrompt(t);
518
+ const pwdOutput = this.extractCommandOutput(originalPrompt, newPrompt, 'pwd');
519
+
520
+ return pwdOutput || process.cwd();
521
+ } catch (error) {
522
+ this.logger.error('Failed to detect current directory', error);
523
+ return process.cwd();
524
+ }
525
+ }
526
+
527
+ /**
528
+ * 获取活跃Shell
529
+ */
530
+ async getActiveShell(terminal?: TerminalTab): Promise<string> {
531
+ const t = terminal || this.getActiveTerminal();
532
+ if (!t) {
533
+ return 'unknown';
534
+ }
535
+
536
+ try {
537
+ // 尝试从环境变量获取
538
+ const shell = process.env.SHELL || process.env.COMSPEC || 'unknown';
539
+
540
+ // 如果无法从环境变量获取,使用 echo $SHELL (Unix) 或 echo %COMSPEC% (Windows)
541
+ const shellCommand = process.platform === 'win32' ? 'echo %COMSPEC%' : 'echo $SHELL';
542
+ this.sendCommandToTerminal(t, shellCommand, true);
543
+
544
+ await new Promise(resolve => setTimeout(resolve, 500));
545
+
546
+ return shell;
547
+ } catch (error) {
548
+ this.logger.error('Failed to detect active shell', error);
549
+ return 'unknown';
550
+ }
551
+ }
552
+
553
+ /**
554
+ * 监控输出流
555
+ */
556
+ monitorOutput(terminal?: TerminalTab): Observable<TerminalOutputEvent> {
557
+ const t = terminal || this.getActiveTerminal();
558
+ if (!t) {
559
+ throw new Error('No terminal available for monitoring');
560
+ }
561
+
562
+ const terminalId = this.getTerminalId(t);
563
+
564
+ // 订阅终端输出
565
+ const subscription = t.output$.subscribe(data => {
566
+ const event: TerminalOutputEvent = {
567
+ terminalId,
568
+ data,
569
+ timestamp: Date.now(),
570
+ type: this.detectOutputType(data)
571
+ };
572
+
573
+ this.outputEventSubject.next(event);
574
+ });
575
+
576
+ this.monitoringIntervals.set(terminalId, subscription);
577
+
578
+ return this.outputEvent$.pipe(
579
+ // 只返回当前终端的事件
580
+ // 注意:实际实现中需要过滤
581
+ );
582
+ }
583
+
584
+ /**
585
+ * 检测提示符
586
+ */
587
+ async getPrompt(terminal?: TerminalTab): Promise<string> {
588
+ const t = terminal || this.getActiveTerminal();
589
+ if (!t) {
590
+ return '';
591
+ }
592
+
593
+ try {
594
+ // 发送一个空命令来触发提示符显示
595
+ this.sendCommandToTerminal(t, '', true);
596
+ await new Promise(resolve => setTimeout(resolve, 200));
597
+
598
+ // 简化实现:返回默认提示符格式
599
+ const shell = await this.getActiveShell(t);
600
+ const cwd = await this.detectCurrentDirectory(t);
601
+
602
+ return this.formatPrompt(shell, cwd);
603
+ } catch (error) {
604
+ this.logger.error('Failed to detect prompt', error);
605
+ return '$ ';
606
+ }
607
+ }
608
+
609
+ /**
610
+ * 追踪进程
611
+ */
612
+ async trackProcesses(terminal?: TerminalTab): Promise<ProcessInfo[]> {
613
+ const t = terminal || this.getActiveTerminal();
614
+ if (!t) {
615
+ return [];
616
+ }
617
+
618
+ try {
619
+ // 使用 ps 命令获取进程列表
620
+ const psCommand = process.platform === 'win32' ? 'tasklist' : 'ps aux';
621
+ this.sendCommandToTerminal(t, psCommand, true);
622
+
623
+ await new Promise(resolve => setTimeout(resolve, 1000));
624
+
625
+ // 简化实现:返回模拟进程信息
626
+ const processes: ProcessInfo[] = [
627
+ {
628
+ pid: process.pid,
629
+ name: process.platform === 'win32' ? 'node.exe' : 'node',
630
+ command: process.argv0,
631
+ status: 'running'
632
+ }
633
+ ];
634
+
635
+ this.processMonitoringSubject.next({
636
+ terminalId: this.getTerminalId(t),
637
+ processes
638
+ });
639
+
640
+ return processes;
641
+ } catch (error) {
642
+ this.logger.error('Failed to track processes', error);
643
+ return [];
644
+ }
645
+ }
646
+
647
+ /**
648
+ * 获取终端AI上下文
649
+ */
650
+ async getTerminalContext(terminal?: TerminalTab): Promise<TerminalContext> {
651
+ const t = terminal || this.getActiveTerminal();
652
+ if (!t) {
653
+ throw new Error('No terminal available');
654
+ }
655
+
656
+ const terminalId = this.getTerminalId(t);
657
+
658
+ // 检查缓存
659
+ if (this.contextCache.has(terminalId)) {
660
+ const cached = this.contextCache.get(terminalId)!;
661
+ // 如果缓存不超过5秒,直接返回
662
+ if (Date.now() - cached.timestamp < 5000) {
663
+ return cached;
664
+ }
665
+ }
666
+
667
+ // 获取最新的上下文信息
668
+ const [currentDirectory, activeShell, prompt, processes] = await Promise.all([
669
+ this.detectCurrentDirectory(t),
670
+ this.getActiveShell(t),
671
+ this.getPrompt(t),
672
+ this.trackProcesses(t)
673
+ ]);
674
+
675
+ const context: TerminalContext = {
676
+ terminalId,
677
+ currentDirectory,
678
+ activeShell,
679
+ prompt,
680
+ processes,
681
+ environment: this.filterEnvVariables(process.env),
682
+ timestamp: Date.now()
683
+ };
684
+
685
+ // 更新缓存
686
+ this.contextCache.set(terminalId, context);
687
+
688
+ return context;
689
+ }
690
+
691
+ /**
692
+ * 清理终端上下文缓存
693
+ */
694
+ clearContextCache(terminalId?: string): void {
695
+ if (terminalId) {
696
+ this.contextCache.delete(terminalId);
697
+ } else {
698
+ this.contextCache.clear();
699
+ }
700
+ }
701
+
702
+ /**
703
+ * 开始持续监控终端
704
+ */
705
+ startContinuousMonitoring(intervalMs: number = 5000): void {
706
+ const terminals = this.getAllTerminals();
707
+
708
+ terminals.forEach(terminal => {
709
+ const terminalId = this.getTerminalId(terminal);
710
+
711
+ // 如果已在监控,先停止
712
+ if (this.monitoringIntervals.has(terminalId)) {
713
+ return;
714
+ }
715
+
716
+ const subscription = interval(intervalMs).subscribe(async () => {
717
+ try {
718
+ await this.getTerminalContext(terminal);
719
+ } catch (error) {
720
+ this.logger.error('Failed to monitor terminal', { terminalId, error });
721
+ }
722
+ });
723
+
724
+ this.monitoringIntervals.set(terminalId, subscription);
725
+ });
726
+
727
+ this.logger.info('Started continuous monitoring', {
728
+ terminalCount: terminals.length,
729
+ intervalMs
730
+ });
731
+ }
732
+
733
+ /**
734
+ * 停止持续监控
735
+ */
736
+ stopContinuousMonitoring(terminalId?: string): void {
737
+ if (terminalId) {
738
+ const subscription = this.monitoringIntervals.get(terminalId);
739
+ if (subscription) {
740
+ subscription.unsubscribe();
741
+ this.monitoringIntervals.delete(terminalId);
742
+ }
743
+ } else {
744
+ this.monitoringIntervals.forEach(sub => sub.unsubscribe());
745
+ this.monitoringIntervals.clear();
746
+ }
747
+
748
+ this.logger.info('Stopped continuous monitoring', { terminalId: terminalId || 'all' });
749
+ }
750
+
751
+ /**
752
+ * 读取终端输出(快捷键功能需要)
753
+ */
754
+ readTerminalOutput(lines: number = 50, terminalIndex?: number): string {
755
+ try {
756
+ const terminals = this.getAllTerminals();
757
+ const terminal = terminalIndex !== undefined
758
+ ? terminals[terminalIndex]
759
+ : this.getActiveTerminal();
760
+
761
+ if (!terminal) {
762
+ return '';
763
+ }
764
+
765
+ // 获取 xterm.js 的 buffer
766
+ const frontend = terminal.frontend as any;
767
+ if (!frontend?._core) {
768
+ return '';
769
+ }
770
+
771
+ const core = frontend._core;
772
+ const buffer = core.buffer || (core.terminal && core.terminal.buffer);
773
+
774
+ if (!buffer) {
775
+ return '';
776
+ }
777
+
778
+ // 获取行数
779
+ const lineCount = buffer.active?.length || buffer.length || 0;
780
+ const startLine = Math.max(0, lineCount - lines);
781
+
782
+ // 收集输出行
783
+ const outputLines: string[] = [];
784
+ for (let i = startLine; i < lineCount; i++) {
785
+ try {
786
+ const line = buffer.active?.getLine(i) || buffer.getLine(i);
787
+ if (line) {
788
+ const lineText = line.translateToString();
789
+ if (lineText) {
790
+ outputLines.push(lineText);
791
+ }
792
+ }
793
+ } catch {
794
+ // 忽略单行读取错误
795
+ }
796
+ }
797
+
798
+ return outputLines.join('\n');
799
+ } catch (error) {
800
+ this.logger.error('Failed to read terminal output', error);
801
+ return '';
802
+ }
803
+ }
804
+
805
+ // ==================== 私有辅助方法 ====================
806
+
807
+ private getTerminalId(terminal: TerminalTab): string {
808
+ return terminal.title || `terminal-${Math.random().toString(36).substr(2, 9)}`;
809
+ }
810
+
811
+ private detectOutputType(data: string): 'output' | 'command' | 'error' | 'prompt' {
812
+ if (data.includes('error') || data.includes('Error') || data.includes('ERROR')) {
813
+ return 'error';
814
+ }
815
+ if (data.includes('$') || data.includes('#') || data.includes('>')) {
816
+ return 'prompt';
817
+ }
818
+ if (data.includes('\r\n') || data.includes('\n')) {
819
+ return 'output';
820
+ }
821
+ return 'command';
822
+ }
823
+
824
+ private formatPrompt(shell: string, cwd: string): string {
825
+ const shellName = shell.split('/').pop() || shell;
826
+ return `${shellName}:${cwd}$ `;
827
+ }
828
+
829
+ private extractCommandOutput(originalPrompt: string, newPrompt: string, command: string): string {
830
+ // 简化实现:实际应该解析终端输出
831
+ const lines = newPrompt.split('\n');
832
+ return lines.find(line => line.includes(command) && !line.includes('$')) || '';
833
+ }
834
+
835
+ private filterEnvVariables(env: NodeJS.ProcessEnv): Record<string, string> {
836
+ const result: Record<string, string> = {};
837
+ for (const key of Object.keys(env)) {
838
+ const value = env[key];
839
+ if (value !== undefined) {
840
+ result[key] = value;
841
+ }
842
+ }
843
+ return result;
844
+ }
845
+ }