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