tabby-ai-assistant 1.0.13 → 1.0.15
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/.editorconfig +18 -0
- package/dist/index.js +1 -1
- package/package.json +6 -4
- package/src/components/chat/ai-sidebar.component.scss +220 -9
- package/src/components/chat/ai-sidebar.component.ts +364 -29
- package/src/components/chat/chat-input.component.ts +36 -4
- package/src/components/chat/chat-interface.component.ts +225 -5
- package/src/components/chat/chat-message.component.ts +6 -1
- package/src/components/settings/context-settings.component.ts +91 -91
- package/src/components/terminal/ai-toolbar-button.component.ts +4 -2
- package/src/components/terminal/command-suggestion.component.ts +148 -6
- package/src/index.ts +0 -6
- package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
- package/src/services/chat/ai-sidebar.service.ts +414 -410
- package/src/services/chat/chat-session.service.ts +36 -12
- package/src/services/context/compaction.ts +110 -134
- package/src/services/context/manager.ts +27 -7
- package/src/services/context/memory.ts +17 -33
- package/src/services/context/summary.service.ts +136 -0
- package/src/services/core/ai-assistant.service.ts +1060 -37
- package/src/services/core/ai-provider-manager.service.ts +154 -25
- package/src/services/core/checkpoint.service.ts +218 -18
- package/src/services/core/toast.service.ts +106 -106
- package/src/services/providers/anthropic-provider.service.ts +126 -30
- package/src/services/providers/base-provider.service.ts +90 -7
- package/src/services/providers/glm-provider.service.ts +151 -38
- package/src/services/providers/minimax-provider.service.ts +55 -40
- package/src/services/providers/ollama-provider.service.ts +117 -28
- package/src/services/providers/openai-compatible.service.ts +164 -34
- package/src/services/providers/openai-provider.service.ts +169 -34
- package/src/services/providers/vllm-provider.service.ts +116 -28
- package/src/services/terminal/terminal-context.service.ts +265 -5
- package/src/services/terminal/terminal-manager.service.ts +748 -748
- package/src/services/terminal/terminal-tools.service.ts +612 -441
- package/src/types/ai.types.ts +156 -3
- package/src/utils/cost.utils.ts +249 -0
- package/src/utils/validation.utils.ts +306 -2
- package/dist/index.js.LICENSE.txt +0 -18
- package/src/services/terminal/command-analyzer.service.ts +0 -43
- package/src/services/terminal/context-menu.service.ts +0 -45
- package/src/services/terminal/hotkey.service.ts +0 -53
|
@@ -1,410 +1,414 @@
|
|
|
1
|
-
import { Injectable, ComponentFactoryResolver, ApplicationRef, Injector, EmbeddedViewRef, ComponentRef } from '@angular/core';
|
|
2
|
-
import { ConfigService } from 'tabby-core';
|
|
3
|
-
import { AiSidebarComponent } from '../../components/chat/ai-sidebar.component';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* AI Sidebar 配置接口
|
|
7
|
-
*/
|
|
8
|
-
export interface AiSidebarConfig {
|
|
9
|
-
enabled?: boolean;
|
|
10
|
-
position?: 'left' | 'right';
|
|
11
|
-
showInToolbar?: boolean;
|
|
12
|
-
sidebarVisible?: boolean;
|
|
13
|
-
sidebarCollapsed?: boolean;
|
|
14
|
-
sidebarWidth?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* AI Sidebar 服务 - 管理 AI 聊天侧边栏的生命周期
|
|
19
|
-
*
|
|
20
|
-
* 采用 Flexbox 布局方式,将 sidebar 插入到 app-root 作为第一个子元素,
|
|
21
|
-
* app-root 变为水平 flex 容器,sidebar 在左侧
|
|
22
|
-
*/
|
|
23
|
-
@Injectable({ providedIn: 'root' })
|
|
24
|
-
export class AiSidebarService {
|
|
25
|
-
private sidebarComponentRef: ComponentRef<AiSidebarComponent> | null = null;
|
|
26
|
-
private sidebarElement: HTMLElement | null = null;
|
|
27
|
-
private styleElement: HTMLStyleElement | null = null;
|
|
28
|
-
private resizeHandle: HTMLElement | null = null;
|
|
29
|
-
private _isVisible = false;
|
|
30
|
-
|
|
31
|
-
// Resize constants
|
|
32
|
-
private readonly MIN_WIDTH = 280;
|
|
33
|
-
private readonly MAX_WIDTH = 500;
|
|
34
|
-
private readonly DEFAULT_WIDTH = 320;
|
|
35
|
-
private currentWidth: number = this.DEFAULT_WIDTH;
|
|
36
|
-
private isResizing = false;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 侧边栏是否可见
|
|
40
|
-
*/
|
|
41
|
-
get sidebarVisible(): boolean {
|
|
42
|
-
return this._isVisible;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
constructor(
|
|
46
|
-
private componentFactoryResolver: ComponentFactoryResolver,
|
|
47
|
-
private appRef: ApplicationRef,
|
|
48
|
-
private injector: Injector,
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
pluginConfig
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
pluginConfig
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
domElem.
|
|
137
|
-
|
|
138
|
-
domElem.style.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
this.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
contentElement.style.
|
|
262
|
-
contentElement.style.
|
|
263
|
-
contentElement.style.
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
contentElement.style.
|
|
274
|
-
contentElement.style.
|
|
275
|
-
contentElement.style.
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
this.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
this.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
this.config.
|
|
409
|
-
|
|
410
|
-
}
|
|
1
|
+
import { Injectable, ComponentFactoryResolver, ApplicationRef, Injector, EmbeddedViewRef, ComponentRef, EnvironmentInjector, createComponent } from '@angular/core';
|
|
2
|
+
import { ConfigService } from 'tabby-core';
|
|
3
|
+
import { AiSidebarComponent } from '../../components/chat/ai-sidebar.component';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AI Sidebar 配置接口
|
|
7
|
+
*/
|
|
8
|
+
export interface AiSidebarConfig {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
position?: 'left' | 'right';
|
|
11
|
+
showInToolbar?: boolean;
|
|
12
|
+
sidebarVisible?: boolean;
|
|
13
|
+
sidebarCollapsed?: boolean;
|
|
14
|
+
sidebarWidth?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* AI Sidebar 服务 - 管理 AI 聊天侧边栏的生命周期
|
|
19
|
+
*
|
|
20
|
+
* 采用 Flexbox 布局方式,将 sidebar 插入到 app-root 作为第一个子元素,
|
|
21
|
+
* app-root 变为水平 flex 容器,sidebar 在左侧
|
|
22
|
+
*/
|
|
23
|
+
@Injectable({ providedIn: 'root' })
|
|
24
|
+
export class AiSidebarService {
|
|
25
|
+
private sidebarComponentRef: ComponentRef<AiSidebarComponent> | null = null;
|
|
26
|
+
private sidebarElement: HTMLElement | null = null;
|
|
27
|
+
private styleElement: HTMLStyleElement | null = null;
|
|
28
|
+
private resizeHandle: HTMLElement | null = null;
|
|
29
|
+
private _isVisible = false;
|
|
30
|
+
|
|
31
|
+
// Resize constants
|
|
32
|
+
private readonly MIN_WIDTH = 280;
|
|
33
|
+
private readonly MAX_WIDTH = 500;
|
|
34
|
+
private readonly DEFAULT_WIDTH = 320;
|
|
35
|
+
private currentWidth: number = this.DEFAULT_WIDTH;
|
|
36
|
+
private isResizing = false;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 侧边栏是否可见
|
|
40
|
+
*/
|
|
41
|
+
get sidebarVisible(): boolean {
|
|
42
|
+
return this._isVisible;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
private componentFactoryResolver: ComponentFactoryResolver,
|
|
47
|
+
private appRef: ApplicationRef,
|
|
48
|
+
private injector: Injector,
|
|
49
|
+
private environmentInjector: EnvironmentInjector,
|
|
50
|
+
private config: ConfigService,
|
|
51
|
+
) { }
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 显示 sidebar
|
|
55
|
+
*/
|
|
56
|
+
show(): void {
|
|
57
|
+
if (this._isVisible) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.createSidebar();
|
|
62
|
+
|
|
63
|
+
const pluginConfig = this.getPluginConfig();
|
|
64
|
+
pluginConfig.sidebarVisible = true;
|
|
65
|
+
this.savePluginConfig(pluginConfig);
|
|
66
|
+
|
|
67
|
+
this._isVisible = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 隐藏 sidebar
|
|
72
|
+
*/
|
|
73
|
+
hide(): void {
|
|
74
|
+
if (!this._isVisible) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.destroySidebar();
|
|
79
|
+
|
|
80
|
+
const pluginConfig = this.getPluginConfig();
|
|
81
|
+
pluginConfig.sidebarVisible = false;
|
|
82
|
+
this.savePluginConfig(pluginConfig);
|
|
83
|
+
|
|
84
|
+
this._isVisible = false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 切换 sidebar 显示状态
|
|
89
|
+
*/
|
|
90
|
+
toggle(): void {
|
|
91
|
+
if (this._isVisible) {
|
|
92
|
+
this.hide();
|
|
93
|
+
} else {
|
|
94
|
+
this.show();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 获取当前显示状态
|
|
100
|
+
*/
|
|
101
|
+
get visible(): boolean {
|
|
102
|
+
return this._isVisible;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 初始化 - 应用启动时调用
|
|
107
|
+
*/
|
|
108
|
+
initialize(): void {
|
|
109
|
+
const pluginConfig = this.getPluginConfig();
|
|
110
|
+
// 默认显示 sidebar,除非明确设置为隐藏
|
|
111
|
+
if (pluginConfig.sidebarVisible !== false) {
|
|
112
|
+
this.show();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 创建 sidebar 组件
|
|
118
|
+
*
|
|
119
|
+
* 使用固定定位方案:
|
|
120
|
+
* 1. 侧边栏 position: fixed,固定在左侧
|
|
121
|
+
* 2. 主内容区通过 margin-left 推开
|
|
122
|
+
* 这样不改变任何现有元素的 flex 布局
|
|
123
|
+
*/
|
|
124
|
+
private createSidebar(): void {
|
|
125
|
+
// 使用 createComponent API (Angular 14+),传入 EnvironmentInjector
|
|
126
|
+
// 这确保组件能正确解析所有 root 级服务依赖
|
|
127
|
+
this.sidebarComponentRef = createComponent(AiSidebarComponent, {
|
|
128
|
+
environmentInjector: this.environmentInjector,
|
|
129
|
+
elementInjector: this.injector
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// 附加到应用
|
|
133
|
+
this.appRef.attachView(this.sidebarComponentRef.hostView);
|
|
134
|
+
|
|
135
|
+
// 获取 DOM 元素
|
|
136
|
+
const domElem = (this.sidebarComponentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
|
|
137
|
+
// 直接设置组件 host 元素的样式 - 确保 flex 布局正确
|
|
138
|
+
domElem.style.display = 'flex';
|
|
139
|
+
domElem.style.flexDirection = 'column';
|
|
140
|
+
domElem.style.height = '100%';
|
|
141
|
+
domElem.style.width = '100%';
|
|
142
|
+
domElem.style.overflow = 'hidden';
|
|
143
|
+
|
|
144
|
+
// 创建 wrapper 元素 - 使用固定定位
|
|
145
|
+
const wrapper = document.createElement('div');
|
|
146
|
+
wrapper.className = 'ai-sidebar-wrapper';
|
|
147
|
+
|
|
148
|
+
// 加载保存的宽度或使用默认值
|
|
149
|
+
this.currentWidth = this.loadSidebarWidth();
|
|
150
|
+
|
|
151
|
+
// 获取视口高度 - 使用绝对像素值确保滚动容器正确计算
|
|
152
|
+
const viewportHeight = window.innerHeight;
|
|
153
|
+
wrapper.style.cssText = `
|
|
154
|
+
position: fixed;
|
|
155
|
+
left: 0;
|
|
156
|
+
top: 0;
|
|
157
|
+
width: ${this.currentWidth}px;
|
|
158
|
+
height: ${viewportHeight}px;
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-direction: column;
|
|
161
|
+
background: var(--bs-body-bg, #1e1e1e);
|
|
162
|
+
border-right: 1px solid var(--bs-border-color, #333);
|
|
163
|
+
box-shadow: 2px 0 10px rgba(0,0,0,0.3);
|
|
164
|
+
z-index: 1000;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
// 监听窗口大小变化,动态更新高度
|
|
169
|
+
const resizeHandler = () => {
|
|
170
|
+
wrapper.style.height = `${window.innerHeight}px`;
|
|
171
|
+
};
|
|
172
|
+
window.addEventListener('resize', resizeHandler);
|
|
173
|
+
// 存储 handler 以便在销毁时移除
|
|
174
|
+
(wrapper as any)._resizeHandler = resizeHandler;
|
|
175
|
+
|
|
176
|
+
// 创建 resize handle(拖动条)
|
|
177
|
+
const resizeHandle = document.createElement('div');
|
|
178
|
+
resizeHandle.className = 'ai-sidebar-resize-handle';
|
|
179
|
+
resizeHandle.style.cssText = `
|
|
180
|
+
position: absolute;
|
|
181
|
+
top: 0;
|
|
182
|
+
right: -4px;
|
|
183
|
+
width: 8px;
|
|
184
|
+
height: 100%;
|
|
185
|
+
cursor: ew-resize;
|
|
186
|
+
background: transparent;
|
|
187
|
+
z-index: 1001;
|
|
188
|
+
transition: background 0.2s;
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
// 鼠标悬停时显示高亮
|
|
192
|
+
resizeHandle.addEventListener('mouseenter', () => {
|
|
193
|
+
resizeHandle.style.background = 'var(--ai-primary, #4dabf7)';
|
|
194
|
+
});
|
|
195
|
+
resizeHandle.addEventListener('mouseleave', () => {
|
|
196
|
+
if (!this.isResizing) {
|
|
197
|
+
resizeHandle.style.background = 'transparent';
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 添加拖动逻辑
|
|
202
|
+
this.setupResizeHandler(resizeHandle, wrapper, viewportHeight);
|
|
203
|
+
|
|
204
|
+
wrapper.appendChild(resizeHandle);
|
|
205
|
+
this.resizeHandle = resizeHandle;
|
|
206
|
+
|
|
207
|
+
wrapper.appendChild(domElem);
|
|
208
|
+
|
|
209
|
+
// 插入到 body
|
|
210
|
+
document.body.appendChild(wrapper);
|
|
211
|
+
|
|
212
|
+
this.sidebarElement = wrapper;
|
|
213
|
+
|
|
214
|
+
// 注入布局 CSS - 只添加 margin-left 把主内容推开
|
|
215
|
+
this.injectLayoutCSS();
|
|
216
|
+
|
|
217
|
+
// 注入服务引用到组件
|
|
218
|
+
if (this.sidebarComponentRef) {
|
|
219
|
+
const component = this.sidebarComponentRef.instance;
|
|
220
|
+
component.sidebarService = this;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 销毁 sidebar 组件
|
|
226
|
+
*/
|
|
227
|
+
private destroySidebar(): void {
|
|
228
|
+
// 移除注入的 CSS
|
|
229
|
+
this.removeLayoutCSS();
|
|
230
|
+
|
|
231
|
+
// 移除 resize 监听器
|
|
232
|
+
if (this.sidebarElement) {
|
|
233
|
+
const handler = (this.sidebarElement as any)._resizeHandler;
|
|
234
|
+
if (handler) {
|
|
235
|
+
window.removeEventListener('resize', handler);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (this.sidebarComponentRef) {
|
|
240
|
+
this.appRef.detachView(this.sidebarComponentRef.hostView);
|
|
241
|
+
this.sidebarComponentRef.destroy();
|
|
242
|
+
this.sidebarComponentRef = null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (this.sidebarElement) {
|
|
246
|
+
this.sidebarElement.remove();
|
|
247
|
+
this.sidebarElement = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 调整 .content 元素样式 - 只处理第二个(更深层的).content
|
|
253
|
+
*/
|
|
254
|
+
private adjustContentStyles(appRoot: Element, apply: boolean): void {
|
|
255
|
+
const contentElements = appRoot.querySelectorAll('.content');
|
|
256
|
+
|
|
257
|
+
if (contentElements.length > 1) {
|
|
258
|
+
// 选择第二个(更深层的).content 元素,这是 Tabby 的主内容区
|
|
259
|
+
const contentElement = contentElements[1] as HTMLElement;
|
|
260
|
+
if (apply) {
|
|
261
|
+
contentElement.style.width = 'auto';
|
|
262
|
+
contentElement.style.flex = '1 1 auto';
|
|
263
|
+
contentElement.style.minWidth = '0';
|
|
264
|
+
} else {
|
|
265
|
+
contentElement.style.removeProperty('width');
|
|
266
|
+
contentElement.style.removeProperty('flex');
|
|
267
|
+
contentElement.style.removeProperty('min-width');
|
|
268
|
+
}
|
|
269
|
+
} else if (contentElements.length === 1) {
|
|
270
|
+
// 如果只有一个 .content,则处理它
|
|
271
|
+
const contentElement = contentElements[0] as HTMLElement;
|
|
272
|
+
if (apply) {
|
|
273
|
+
contentElement.style.width = 'auto';
|
|
274
|
+
contentElement.style.flex = '1 1 auto';
|
|
275
|
+
contentElement.style.minWidth = '0';
|
|
276
|
+
} else {
|
|
277
|
+
contentElement.style.removeProperty('width');
|
|
278
|
+
contentElement.style.removeProperty('flex');
|
|
279
|
+
contentElement.style.removeProperty('min-width');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 注入布局 CSS - 使用 margin-left 把主内容推开
|
|
286
|
+
*
|
|
287
|
+
* 固定定位方案:侧边栏 fixed,主内容区 margin-left
|
|
288
|
+
*/
|
|
289
|
+
private injectLayoutCSS(): void {
|
|
290
|
+
const style = document.createElement('style');
|
|
291
|
+
style.id = 'ai-sidebar-layout-css';
|
|
292
|
+
style.textContent = `
|
|
293
|
+
/* 用 margin-left 把 app-root 推开,为侧边栏腾出空间 */
|
|
294
|
+
app-root {
|
|
295
|
+
margin-left: ${this.currentWidth}px !important;
|
|
296
|
+
}
|
|
297
|
+
`;
|
|
298
|
+
|
|
299
|
+
document.head.appendChild(style);
|
|
300
|
+
this.styleElement = style;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 移除布局 CSS
|
|
305
|
+
*/
|
|
306
|
+
private removeLayoutCSS(): void {
|
|
307
|
+
if (this.styleElement) {
|
|
308
|
+
this.styleElement.remove();
|
|
309
|
+
this.styleElement = null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* 设置 resize handle 拖动逻辑
|
|
315
|
+
*/
|
|
316
|
+
private setupResizeHandler(handle: HTMLElement, wrapper: HTMLElement, viewportHeight: number): void {
|
|
317
|
+
let startX: number;
|
|
318
|
+
let startWidth: number;
|
|
319
|
+
|
|
320
|
+
const onMouseDown = (e: MouseEvent) => {
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
this.isResizing = true;
|
|
323
|
+
startX = e.clientX;
|
|
324
|
+
startWidth = this.currentWidth;
|
|
325
|
+
|
|
326
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
327
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
328
|
+
document.body.style.cursor = 'ew-resize';
|
|
329
|
+
document.body.style.userSelect = 'none';
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
333
|
+
if (!this.isResizing) return;
|
|
334
|
+
|
|
335
|
+
const delta = e.clientX - startX;
|
|
336
|
+
let newWidth = startWidth + delta;
|
|
337
|
+
|
|
338
|
+
// 限制宽度范围
|
|
339
|
+
newWidth = Math.max(this.MIN_WIDTH, Math.min(this.MAX_WIDTH, newWidth));
|
|
340
|
+
|
|
341
|
+
this.currentWidth = newWidth;
|
|
342
|
+
wrapper.style.width = `${newWidth}px`;
|
|
343
|
+
|
|
344
|
+
// 更新 app-root 的 margin-left
|
|
345
|
+
this.updateLayoutCSS(newWidth);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const onMouseUp = () => {
|
|
349
|
+
this.isResizing = false;
|
|
350
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
351
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
352
|
+
document.body.style.cursor = '';
|
|
353
|
+
document.body.style.userSelect = '';
|
|
354
|
+
handle.style.background = 'transparent';
|
|
355
|
+
|
|
356
|
+
// 保存宽度到配置
|
|
357
|
+
this.saveSidebarWidth(this.currentWidth);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
handle.addEventListener('mousedown', onMouseDown);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* 更新布局 CSS(resize 时调用)
|
|
365
|
+
*/
|
|
366
|
+
private updateLayoutCSS(width: number): void {
|
|
367
|
+
if (this.styleElement) {
|
|
368
|
+
this.styleElement.textContent = `
|
|
369
|
+
app-root {
|
|
370
|
+
margin-left: ${width}px !important;
|
|
371
|
+
}
|
|
372
|
+
`;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* 加载保存的侧边栏宽度
|
|
378
|
+
*/
|
|
379
|
+
private loadSidebarWidth(): number {
|
|
380
|
+
const pluginConfig = this.getPluginConfig();
|
|
381
|
+
const savedWidth = pluginConfig.sidebarWidth;
|
|
382
|
+
if (savedWidth && savedWidth >= this.MIN_WIDTH && savedWidth <= this.MAX_WIDTH) {
|
|
383
|
+
return savedWidth;
|
|
384
|
+
}
|
|
385
|
+
return this.DEFAULT_WIDTH;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* 保存侧边栏宽度到配置
|
|
390
|
+
*/
|
|
391
|
+
private saveSidebarWidth(width: number): void {
|
|
392
|
+
const pluginConfig = this.getPluginConfig();
|
|
393
|
+
pluginConfig.sidebarWidth = width;
|
|
394
|
+
this.savePluginConfig(pluginConfig);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 获取插件配置
|
|
399
|
+
*/
|
|
400
|
+
private getPluginConfig(): AiSidebarConfig {
|
|
401
|
+
return this.config.store.pluginConfig?.['ai-assistant'] || {};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 保存插件配置
|
|
406
|
+
*/
|
|
407
|
+
private savePluginConfig(pluginConfig: AiSidebarConfig): void {
|
|
408
|
+
if (!this.config.store.pluginConfig) {
|
|
409
|
+
this.config.store.pluginConfig = {};
|
|
410
|
+
}
|
|
411
|
+
this.config.store.pluginConfig['ai-assistant'] = pluginConfig;
|
|
412
|
+
this.config.save();
|
|
413
|
+
}
|
|
414
|
+
}
|