tabby-ai-assistant 1.0.15 → 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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabby-ai-assistant",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "Tabby终端AI助手插件 - 支持多AI提供商(OpenAI、Anthropic、Minimax、GLM、Ollama、vLLM)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -70,4 +70,4 @@
|
|
|
70
70
|
"webpack": "^5.24.4",
|
|
71
71
|
"webpack-cli": "^4.5.0"
|
|
72
72
|
}
|
|
73
|
-
}
|
|
73
|
+
}
|
|
@@ -220,6 +220,21 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
|
|
|
220
220
|
this.sendWelcomeMessage();
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// 订阅预设消息(快捷键功能)
|
|
224
|
+
this.sidebarService.presetMessage$.pipe(
|
|
225
|
+
takeUntil(this.destroy$)
|
|
226
|
+
).subscribe(({ message, autoSend }) => {
|
|
227
|
+
this.inputValue = message;
|
|
228
|
+
|
|
229
|
+
if (autoSend) {
|
|
230
|
+
// 延迟一点确保 UI 更新
|
|
231
|
+
setTimeout(() => this.submit(), 100);
|
|
232
|
+
} else {
|
|
233
|
+
// 聚焦输入框
|
|
234
|
+
this.textInput?.nativeElement?.focus();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
223
238
|
// 延迟检查滚动状态(等待 DOM 渲染)
|
|
224
239
|
setTimeout(() => this.checkScrollState(), 100);
|
|
225
240
|
}
|
package/src/index.ts
CHANGED
|
@@ -204,6 +204,7 @@ export default class AiAssistantModule {
|
|
|
204
204
|
private config: ConfigService,
|
|
205
205
|
private aiService: AiAssistantService,
|
|
206
206
|
private sidebarService: AiSidebarService,
|
|
207
|
+
private terminalManager: TerminalManagerService,
|
|
207
208
|
hotkeys: HotkeysService
|
|
208
209
|
) {
|
|
209
210
|
console.log('[AiAssistantModule] Module initialized');
|
|
@@ -224,21 +225,88 @@ export default class AiAssistantModule {
|
|
|
224
225
|
|
|
225
226
|
// 订阅热键事件
|
|
226
227
|
hotkeys.hotkey$.subscribe(hotkey => {
|
|
227
|
-
|
|
228
|
-
this.sidebarService.toggle();
|
|
229
|
-
} else if (hotkey === 'ai-command-generation') {
|
|
230
|
-
// 打开侧边栏并聚焦输入
|
|
231
|
-
if (!this.sidebarService.sidebarVisible) {
|
|
232
|
-
this.sidebarService.show();
|
|
233
|
-
}
|
|
234
|
-
} else if (hotkey === 'ai-explain-command') {
|
|
235
|
-
// 打开侧边栏
|
|
236
|
-
if (!this.sidebarService.sidebarVisible) {
|
|
237
|
-
this.sidebarService.show();
|
|
238
|
-
}
|
|
239
|
-
}
|
|
228
|
+
this.handleHotkey(hotkey);
|
|
240
229
|
});
|
|
241
230
|
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 处理热键事件
|
|
234
|
+
*/
|
|
235
|
+
private handleHotkey(hotkey: string): void {
|
|
236
|
+
switch (hotkey) {
|
|
237
|
+
case 'ai-assistant-toggle':
|
|
238
|
+
this.sidebarService.toggle();
|
|
239
|
+
break;
|
|
240
|
+
|
|
241
|
+
case 'ai-command-generation':
|
|
242
|
+
this.handleCommandGeneration();
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case 'ai-explain-command':
|
|
246
|
+
this.handleExplainCommand();
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 处理命令生成快捷键 (Ctrl+Shift+G)
|
|
253
|
+
* 1. 尝试获取选中文本
|
|
254
|
+
* 2. 尝试获取最后一条命令
|
|
255
|
+
* 3. 获取终端上下文
|
|
256
|
+
* 4. 构建提示并发送
|
|
257
|
+
*/
|
|
258
|
+
private handleCommandGeneration(): void {
|
|
259
|
+
// 1. 尝试获取选中文本
|
|
260
|
+
const selectedText = this.terminalManager.getSelectedText();
|
|
261
|
+
|
|
262
|
+
// 2. 尝试获取最后一条命令
|
|
263
|
+
const lastCommand = this.terminalManager.getLastCommand();
|
|
264
|
+
|
|
265
|
+
// 3. 获取终端上下文
|
|
266
|
+
const context = this.terminalManager.getRecentContext();
|
|
267
|
+
|
|
268
|
+
// 4. 构建提示
|
|
269
|
+
let prompt: string;
|
|
270
|
+
if (selectedText) {
|
|
271
|
+
prompt = `请帮我优化或改进这个命令:\n\`\`\`\n${selectedText}\n\`\`\``;
|
|
272
|
+
} else if (lastCommand) {
|
|
273
|
+
prompt = `基于当前终端状态,请帮我生成下一步需要的命令。\n\n最近执行的命令: ${lastCommand}\n\n终端上下文:\n\`\`\`\n${context}\n\`\`\``;
|
|
274
|
+
} else {
|
|
275
|
+
prompt = `请根据当前终端状态帮我生成需要的命令。\n\n终端上下文:\n\`\`\`\n${context}\n\`\`\``;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 5. 发送消息(自动发送)
|
|
279
|
+
this.sidebarService.sendPresetMessage(prompt, true);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 处理命令解释快捷键 (Ctrl+Shift+E)
|
|
284
|
+
* 1. 尝试获取选中文本
|
|
285
|
+
* 2. 尝试获取最后一条命令
|
|
286
|
+
* 3. 构建提示并发送
|
|
287
|
+
*/
|
|
288
|
+
private handleExplainCommand(): void {
|
|
289
|
+
// 1. 尝试获取选中文本
|
|
290
|
+
const selectedText = this.terminalManager.getSelectedText();
|
|
291
|
+
|
|
292
|
+
// 2. 尝试获取最后一条命令
|
|
293
|
+
const lastCommand = this.terminalManager.getLastCommand();
|
|
294
|
+
|
|
295
|
+
// 3. 构建提示
|
|
296
|
+
let prompt: string;
|
|
297
|
+
if (selectedText) {
|
|
298
|
+
prompt = `请详细解释这个命令的作用和每个参数的含义:\n\`\`\`\n${selectedText}\n\`\`\``;
|
|
299
|
+
} else if (lastCommand) {
|
|
300
|
+
prompt = `请详细解释这个命令的作用和每个参数的含义:\n\`\`\`\n${lastCommand}\n\`\`\``;
|
|
301
|
+
} else {
|
|
302
|
+
// 读取更多上下文让用户选择
|
|
303
|
+
const context = this.terminalManager.getRecentContext();
|
|
304
|
+
prompt = `请解释最近的终端输出内容:\n\`\`\`\n${context}\n\`\`\``;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 4. 发送消息(自动发送)
|
|
308
|
+
this.sidebarService.sendPresetMessage(prompt, true);
|
|
309
|
+
}
|
|
242
310
|
}
|
|
243
311
|
|
|
244
312
|
export const forRoot = (): typeof AiAssistantModule => {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { Injectable, ComponentFactoryResolver, ApplicationRef, Injector, EmbeddedViewRef, ComponentRef, EnvironmentInjector, createComponent } from '@angular/core';
|
|
2
|
+
import { Subject, Observable } from 'rxjs';
|
|
2
3
|
import { ConfigService } from 'tabby-core';
|
|
3
4
|
import { AiSidebarComponent } from '../../components/chat/ai-sidebar.component';
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* 预设消息接口
|
|
8
|
+
*/
|
|
9
|
+
export interface PresetMessage {
|
|
10
|
+
message: string;
|
|
11
|
+
autoSend: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
/**
|
|
6
15
|
* AI Sidebar 配置接口
|
|
7
16
|
*/
|
|
@@ -35,6 +44,16 @@ export class AiSidebarService {
|
|
|
35
44
|
private currentWidth: number = this.DEFAULT_WIDTH;
|
|
36
45
|
private isResizing = false;
|
|
37
46
|
|
|
47
|
+
// 预设消息 Subject(用于快捷键功能)
|
|
48
|
+
private presetMessageSubject = new Subject<PresetMessage>();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 获取预设消息 Observable
|
|
52
|
+
*/
|
|
53
|
+
get presetMessage$(): Observable<PresetMessage> {
|
|
54
|
+
return this.presetMessageSubject.asObservable();
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
/**
|
|
39
58
|
* 侧边栏是否可见
|
|
40
59
|
*/
|
|
@@ -102,6 +121,21 @@ export class AiSidebarService {
|
|
|
102
121
|
return this._isVisible;
|
|
103
122
|
}
|
|
104
123
|
|
|
124
|
+
/**
|
|
125
|
+
* 发送预设消息并执行
|
|
126
|
+
* 用于快捷键功能 - 自动填充并发送消息
|
|
127
|
+
* @param message 消息内容
|
|
128
|
+
* @param autoSend 是否自动发送(否则只填充不发送)
|
|
129
|
+
*/
|
|
130
|
+
sendPresetMessage(message: string, autoSend: boolean = true): void {
|
|
131
|
+
if (!this._isVisible) {
|
|
132
|
+
this.show();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 通知侧边栏组件填充消息
|
|
136
|
+
this.presetMessageSubject.next({ message, autoSend });
|
|
137
|
+
}
|
|
138
|
+
|
|
105
139
|
/**
|
|
106
140
|
* 初始化 - 应用启动时调用
|
|
107
141
|
*/
|
|
@@ -254,6 +254,49 @@ export class TerminalManagerService {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
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
|
+
|
|
257
300
|
/**
|
|
258
301
|
* 获取终端工作目录
|
|
259
302
|
*/
|
|
@@ -705,6 +748,60 @@ export class TerminalManagerService {
|
|
|
705
748
|
this.logger.info('Stopped continuous monitoring', { terminalId: terminalId || 'all' });
|
|
706
749
|
}
|
|
707
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
|
+
|
|
708
805
|
// ==================== 私有辅助方法 ====================
|
|
709
806
|
|
|
710
807
|
private getTerminalId(terminal: TerminalTab): string {
|