tabby-ai-assistant 1.0.8 → 1.0.10

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 (54) hide show
  1. package/dist/components/chat/ai-sidebar.component.d.ts +16 -3
  2. package/dist/components/chat/chat-input.component.d.ts +4 -0
  3. package/dist/components/chat/chat-interface.component.d.ts +22 -1
  4. package/dist/components/chat/chat-settings.component.d.ts +21 -11
  5. package/dist/components/settings/ai-settings-tab.component.d.ts +14 -4
  6. package/dist/components/settings/general-settings.component.d.ts +43 -12
  7. package/dist/components/settings/provider-config.component.d.ts +110 -5
  8. package/dist/components/settings/security-settings.component.d.ts +14 -4
  9. package/dist/i18n/index.d.ts +48 -0
  10. package/dist/i18n/translations/en-US.d.ts +5 -0
  11. package/dist/i18n/translations/ja-JP.d.ts +5 -0
  12. package/dist/i18n/translations/zh-CN.d.ts +5 -0
  13. package/dist/i18n/types.d.ts +198 -0
  14. package/dist/index.js +1 -1
  15. package/dist/services/chat/ai-sidebar.service.d.ts +23 -1
  16. package/dist/services/core/theme.service.d.ts +53 -0
  17. package/dist/services/core/toast.service.d.ts +15 -0
  18. package/package.json +1 -1
  19. package/src/components/chat/ai-sidebar.component.scss +468 -0
  20. package/src/components/chat/ai-sidebar.component.ts +47 -344
  21. package/src/components/chat/chat-input.component.scss +2 -2
  22. package/src/components/chat/chat-input.component.ts +16 -5
  23. package/src/components/chat/chat-interface.component.html +11 -11
  24. package/src/components/chat/chat-interface.component.scss +410 -4
  25. package/src/components/chat/chat-interface.component.ts +105 -14
  26. package/src/components/chat/chat-message.component.scss +3 -3
  27. package/src/components/chat/chat-message.component.ts +3 -2
  28. package/src/components/chat/chat-settings.component.html +95 -61
  29. package/src/components/chat/chat-settings.component.scss +224 -50
  30. package/src/components/chat/chat-settings.component.ts +56 -30
  31. package/src/components/security/risk-confirm-dialog.component.scss +7 -7
  32. package/src/components/settings/ai-settings-tab.component.html +27 -27
  33. package/src/components/settings/ai-settings-tab.component.scss +34 -20
  34. package/src/components/settings/ai-settings-tab.component.ts +59 -20
  35. package/src/components/settings/general-settings.component.html +69 -40
  36. package/src/components/settings/general-settings.component.scss +151 -58
  37. package/src/components/settings/general-settings.component.ts +168 -55
  38. package/src/components/settings/provider-config.component.html +183 -60
  39. package/src/components/settings/provider-config.component.scss +332 -153
  40. package/src/components/settings/provider-config.component.ts +268 -19
  41. package/src/components/settings/security-settings.component.html +70 -39
  42. package/src/components/settings/security-settings.component.scss +104 -8
  43. package/src/components/settings/security-settings.component.ts +48 -10
  44. package/src/i18n/index.ts +129 -0
  45. package/src/i18n/translations/en-US.ts +193 -0
  46. package/src/i18n/translations/ja-JP.ts +193 -0
  47. package/src/i18n/translations/zh-CN.ts +193 -0
  48. package/src/i18n/types.ts +224 -0
  49. package/src/index.ts +6 -0
  50. package/src/services/chat/ai-sidebar.service.ts +157 -5
  51. package/src/services/core/theme.service.ts +480 -0
  52. package/src/services/core/toast.service.ts +36 -0
  53. package/src/styles/ai-assistant.scss +8 -88
  54. package/src/styles/themes.scss +161 -0
@@ -11,6 +11,7 @@ export interface AiSidebarConfig {
11
11
  showInToolbar?: boolean;
12
12
  sidebarVisible?: boolean;
13
13
  sidebarCollapsed?: boolean;
14
+ sidebarWidth?: number;
14
15
  }
15
16
 
16
17
  /**
@@ -24,8 +25,15 @@ export class AiSidebarService {
24
25
  private sidebarComponentRef: ComponentRef<AiSidebarComponent> | null = null;
25
26
  private sidebarElement: HTMLElement | null = null;
26
27
  private styleElement: HTMLStyleElement | null = null;
28
+ private resizeHandle: HTMLElement | null = null;
27
29
  private _isVisible = false;
28
- private readonly SIDEBAR_WIDTH = 320;
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;
29
37
 
30
38
  /**
31
39
  * 侧边栏是否可见
@@ -122,24 +130,76 @@ export class AiSidebarService {
122
130
 
123
131
  // 获取 DOM 元素
124
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';
125
139
 
126
140
  // 创建 wrapper 元素 - 使用固定定位
127
141
  const wrapper = document.createElement('div');
128
142
  wrapper.className = 'ai-sidebar-wrapper';
143
+
144
+ // 加载保存的宽度或使用默认值
145
+ this.currentWidth = this.loadSidebarWidth();
146
+
147
+ // 获取视口高度 - 使用绝对像素值确保滚动容器正确计算
148
+ const viewportHeight = window.innerHeight;
129
149
  wrapper.style.cssText = `
130
150
  position: fixed;
131
151
  left: 0;
132
152
  top: 0;
133
- width: ${this.SIDEBAR_WIDTH}px;
134
- height: 100%;
153
+ width: ${this.currentWidth}px;
154
+ height: ${viewportHeight}px;
135
155
  display: flex;
136
156
  flex-direction: column;
137
157
  background: var(--bs-body-bg, #1e1e1e);
138
158
  border-right: 1px solid var(--bs-border-color, #333);
139
159
  box-shadow: 2px 0 10px rgba(0,0,0,0.3);
140
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;
141
185
  `;
142
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
+
143
203
  wrapper.appendChild(domElem);
144
204
 
145
205
  // 插入到 body
@@ -164,6 +224,14 @@ export class AiSidebarService {
164
224
  // 移除注入的 CSS
165
225
  this.removeLayoutCSS();
166
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
+
167
235
  if (this.sidebarComponentRef) {
168
236
  this.appRef.detachView(this.sidebarComponentRef.hostView);
169
237
  this.sidebarComponentRef.destroy();
@@ -211,7 +279,7 @@ export class AiSidebarService {
211
279
 
212
280
  /**
213
281
  * 注入布局 CSS - 使用 margin-left 把主内容推开
214
- *
282
+ *
215
283
  * 固定定位方案:侧边栏 fixed,主内容区 margin-left
216
284
  */
217
285
  private injectLayoutCSS(): void {
@@ -220,7 +288,7 @@ export class AiSidebarService {
220
288
  style.textContent = `
221
289
  /* 用 margin-left 把 app-root 推开,为侧边栏腾出空间 */
222
290
  app-root {
223
- margin-left: ${this.SIDEBAR_WIDTH}px !important;
291
+ margin-left: ${this.currentWidth}px !important;
224
292
  }
225
293
  `;
226
294
 
@@ -238,6 +306,90 @@ export class AiSidebarService {
238
306
  }
239
307
  }
240
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
+
241
393
  /**
242
394
  * 获取插件配置
243
395
  */
@@ -0,0 +1,480 @@
1
+ /**
2
+ * 主题服务 - 统一管理所有 AI 助手主题
3
+ * 通过动态 <style> 注入实现主题切换
4
+ */
5
+ import { Injectable, OnDestroy } from '@angular/core';
6
+ import { Subject, Subscription } from 'rxjs';
7
+ import { debounceTime } from 'rxjs/operators';
8
+ import { ConfigService } from 'tabby-core';
9
+ import { ConfigProviderService } from './config-provider.service';
10
+
11
+ export type ThemeType = 'auto' | 'light' | 'dark' | 'pixel' | 'tech';
12
+
13
+ // 主题变量定义
14
+ const THEME_VARIABLES: Record<Exclude<ThemeType, 'auto'>, Record<string, string>> = {
15
+ light: {
16
+ // 主色调
17
+ 'ai-primary': '#007bff',
18
+ 'ai-primary-hover': '#0056b3',
19
+ 'ai-secondary': '#6c757d',
20
+ 'ai-success': '#28a745',
21
+ 'ai-warning': '#ffc107',
22
+ 'ai-danger': '#dc3545',
23
+ 'ai-info': '#17a2b8',
24
+ // 风险级别颜色
25
+ 'ai-risk-low': '#28a745',
26
+ 'ai-risk-medium': '#ffc107',
27
+ 'ai-risk-high': '#fd7e14',
28
+ 'ai-risk-critical': '#dc3545',
29
+ // 聊天消息颜色
30
+ 'ai-user-message': '#e3f2fd',
31
+ 'ai-assistant-message': '#f5f5f5',
32
+ 'ai-system-message': '#fff3cd',
33
+ // 背景和边框
34
+ 'ai-bg-primary': '#ffffff',
35
+ 'ai-bg-secondary': '#f8f9fa',
36
+ 'ai-bg-tertiary': '#e9ecef',
37
+ 'ai-text-primary': '#212529',
38
+ 'ai-text-secondary': '#6c757d',
39
+ 'ai-border': '#dee2e6',
40
+ 'ai-border-radius': '0.375rem',
41
+ 'ai-box-shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
42
+ // 字体
43
+ 'ai-font-family': "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
44
+ 'ai-font-size-base': '14px',
45
+ // 其他
46
+ 'ai-dark': '#212529',
47
+ 'ai-light': '#f8f9fa',
48
+ 'ai-transition-duration': '0.3s'
49
+ },
50
+ dark: {
51
+ // 主色调
52
+ 'ai-primary': '#4dabf7',
53
+ 'ai-primary-hover': '#339af0',
54
+ 'ai-secondary': '#adb5bd',
55
+ 'ai-success': '#51cf66',
56
+ 'ai-warning': '#fcc419',
57
+ 'ai-danger': '#ff6b6b',
58
+ 'ai-info': '#22b8cf',
59
+ // 风险级别颜色
60
+ 'ai-risk-low': '#51cf66',
61
+ 'ai-risk-medium': '#fcc419',
62
+ 'ai-risk-high': '#ff922b',
63
+ 'ai-risk-critical': '#ff6b6b',
64
+ // 聊天消息颜色
65
+ 'ai-user-message': '#1e3a5f',
66
+ 'ai-assistant-message': '#2d3748',
67
+ 'ai-system-message': '#3a3a3a',
68
+ // 背景和边框
69
+ 'ai-bg-primary': '#1a1a1a',
70
+ 'ai-bg-secondary': '#2d2d2d',
71
+ 'ai-bg-tertiary': '#3d3d3d',
72
+ 'ai-text-primary': '#f8f9fa',
73
+ 'ai-text-secondary': '#adb5bd',
74
+ 'ai-border': '#4a4a4a',
75
+ 'ai-border-radius': '0.375rem',
76
+ 'ai-box-shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.3)',
77
+ // 字体
78
+ 'ai-font-family': "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
79
+ 'ai-font-size-base': '14px',
80
+ // 其他
81
+ 'ai-dark': '#212529',
82
+ 'ai-light': '#f8f9fa',
83
+ 'ai-transition-duration': '0.3s'
84
+ },
85
+ pixel: {
86
+ // 主色调 - 经典 GameBoy 绿
87
+ 'ai-primary': '#9bbc0f',
88
+ 'ai-primary-hover': '#8bac0f',
89
+ 'ai-secondary': '#306230',
90
+ 'ai-success': '#9bbc0f',
91
+ 'ai-warning': '#ffeb3b',
92
+ 'ai-danger': '#f44336',
93
+ 'ai-info': '#03a9f4',
94
+ // 风险级别颜色
95
+ 'ai-risk-low': '#9bbc0f',
96
+ 'ai-risk-medium': '#ffeb3b',
97
+ 'ai-risk-high': '#ff9800',
98
+ 'ai-risk-critical': '#f44336',
99
+ // 聊天消息颜色
100
+ 'ai-user-message': '#0f380f',
101
+ 'ai-assistant-message': '#306230',
102
+ 'ai-system-message': '#1a1a1a',
103
+ // 背景和边框
104
+ 'ai-bg-primary': '#0f380f',
105
+ 'ai-bg-secondary': '#1a2a1a',
106
+ 'ai-bg-tertiary': '#306230',
107
+ 'ai-text-primary': '#9bbc0f',
108
+ 'ai-text-secondary': '#8bac0f',
109
+ 'ai-border': '#9bbc0f',
110
+ 'ai-border-radius': '0',
111
+ 'ai-box-shadow': '4px 4px 0 rgba(15, 56, 15, 0.8)',
112
+ // 字体 - 像素风格
113
+ 'ai-font-family': "'Courier New', 'Press Start 2P', monospace",
114
+ 'ai-font-size-base': '12px',
115
+ // 其他
116
+ 'ai-dark': '#0f380f',
117
+ 'ai-light': '#306230',
118
+ 'ai-transition-duration': '0s'
119
+ },
120
+ tech: {
121
+ // 主色调 - 霓虹赛博朋克
122
+ 'ai-primary': '#00fff9',
123
+ 'ai-primary-hover': '#00e6e0',
124
+ 'ai-secondary': '#adb5bd',
125
+ 'ai-success': '#00ff88',
126
+ 'ai-warning': '#ff00ff',
127
+ 'ai-danger': '#ff3366',
128
+ 'ai-info': '#00bfff',
129
+ // 风险级别颜色
130
+ 'ai-risk-low': '#00ff88',
131
+ 'ai-risk-medium': '#ff00ff',
132
+ 'ai-risk-high': '#ff6600',
133
+ 'ai-risk-critical': '#ff3366',
134
+ // 聊天消息颜色
135
+ 'ai-user-message': 'rgba(0, 255, 249, 0.1)',
136
+ 'ai-assistant-message': 'rgba(255, 0, 255, 0.1)',
137
+ 'ai-system-message': 'rgba(0, 255, 136, 0.1)',
138
+ // 背景和边框
139
+ 'ai-bg-primary': '#0a0a0f',
140
+ 'ai-bg-secondary': '#12121a',
141
+ 'ai-bg-tertiary': '#1a1a2e',
142
+ 'ai-text-primary': '#00fff9',
143
+ 'ai-text-secondary': 'rgba(0, 255, 249, 0.7)',
144
+ 'ai-border': 'rgba(0, 255, 249, 0.3)',
145
+ 'ai-border-radius': '4px',
146
+ 'ai-box-shadow': '0 0 20px rgba(0, 255, 249, 0.2)',
147
+ // 字体 - 科幻感
148
+ 'ai-font-family': "'Segoe UI', 'Share Tech Mono', monospace",
149
+ 'ai-font-size-base': '14px',
150
+ // 其他
151
+ 'ai-dark': '#0a0a0f',
152
+ 'ai-light': '#12121a',
153
+ 'ai-transition-duration': '0.3s'
154
+ }
155
+ };
156
+
157
+ @Injectable({
158
+ providedIn: 'root'
159
+ })
160
+ export class ThemeService implements OnDestroy {
161
+ private currentTheme$ = new Subject<ThemeType>();
162
+ private tabbySubscription?: Subscription;
163
+ private styleElement: HTMLStyleElement;
164
+ readonly theme$ = this.currentTheme$.asObservable();
165
+
166
+ private readonly allThemeClasses = [
167
+ 'ai-theme-auto', 'ai-theme-light', 'ai-theme-dark',
168
+ 'ai-theme-pixel', 'ai-theme-tech'
169
+ ];
170
+
171
+ // AI 助手容器选择器
172
+ private readonly containerSelectors = [
173
+ '.ai-chat-interface',
174
+ '.ai-settings-tab',
175
+ '.ai-assistant',
176
+ '.ai-sidebar-container',
177
+ '.ai-chat-modal-left'
178
+ ];
179
+
180
+ constructor(
181
+ private config: ConfigProviderService,
182
+ private tabbyConfig: ConfigService
183
+ ) {
184
+ // 创建并注入动态样式元素
185
+ this.styleElement = document.createElement('style');
186
+ this.styleElement.id = 'ai-assistant-dynamic-theme';
187
+ document.head.appendChild(this.styleElement);
188
+
189
+ this.init();
190
+ }
191
+
192
+ private init(): void {
193
+ const savedTheme = this.config.get<string>('theme', 'auto') as ThemeType;
194
+ this.setTheme(savedTheme);
195
+
196
+ // 监听 Tabby 主题变化(带防抖)
197
+ this.tabbySubscription = this.tabbyConfig.changed$.pipe(
198
+ debounceTime(100)
199
+ ).subscribe(() => {
200
+ const currentTheme = this.config.get<string>('theme', 'auto');
201
+ if (currentTheme === 'auto') {
202
+ this.applyTheme('auto');
203
+ }
204
+ });
205
+ }
206
+
207
+ /**
208
+ * 获取当前主题
209
+ */
210
+ getCurrentTheme(): ThemeType {
211
+ return this.config.get<string>('theme', 'auto') as ThemeType;
212
+ }
213
+
214
+ /**
215
+ * 设置并应用主题
216
+ */
217
+ setTheme(theme: ThemeType): void {
218
+ this.config.set('theme', theme);
219
+ this.applyTheme(theme);
220
+ }
221
+
222
+ /**
223
+ * 核心方法:动态注入主题样式
224
+ */
225
+ applyTheme(theme: ThemeType): void {
226
+ this.currentTheme$.next(theme);
227
+
228
+ // 确定实际生效的主题
229
+ let effectiveTheme: ThemeType = theme;
230
+ if (theme === 'auto') {
231
+ effectiveTheme = this.getTabbyEffectiveTheme();
232
+ }
233
+
234
+ // 1. 生成 CSS 变量样式
235
+ const cssVariables = this.buildCssVariables(effectiveTheme);
236
+ // 2. 生成主题特定样式
237
+ const themeStyles = this.buildThemeStyles(effectiveTheme);
238
+ // 3. 注入完整样式到 DOM
239
+ this.styleElement.innerHTML = `
240
+ /* AI Assistant Dynamic Theme - ${theme} (effective: ${effectiveTheme}) */
241
+ :root,
242
+ html,
243
+ body,
244
+ ${this.containerSelectors.join(',\n')} {
245
+ ${cssVariables}
246
+ }
247
+ ${themeStyles}
248
+ `.trim();
249
+
250
+ // 4. 更新类名和 data 属性
251
+ this.updateBodyClasses(theme, effectiveTheme);
252
+
253
+ // 5. 触发自定义事件
254
+ window.dispatchEvent(new CustomEvent('ai-theme-changed', {
255
+ detail: { theme, effectiveTheme }
256
+ }));
257
+
258
+ console.log('[ThemeService] Theme applied dynamically:', { theme, effectiveTheme });
259
+ }
260
+
261
+ /**
262
+ * 生成 CSS 变量字符串
263
+ */
264
+ private buildCssVariables(theme: ThemeType): string {
265
+ const vars = THEME_VARIABLES[theme] || THEME_VARIABLES.dark;
266
+ return Object.entries(vars)
267
+ .map(([key, value]) => ` --${key}: ${value} !important;`)
268
+ .join('\n');
269
+ }
270
+
271
+ /**
272
+ * 生成主题特定样式(像素风格、科技风格等)
273
+ */
274
+ private buildThemeStyles(theme: ThemeType): string {
275
+ if (theme === 'pixel') {
276
+ return `
277
+ /* Pixel theme specific styles */
278
+ ${this.containerSelectors.join(',\n')} {
279
+ /* 按钮像素化 */
280
+ .btn {
281
+ border: 3px solid var(--ai-border) !important;
282
+ border-radius: 0 !important;
283
+ box-shadow: 4px 4px 0 var(--ai-bg-tertiary) !important;
284
+ font-family: var(--ai-font-family) !important;
285
+ transition: none !important;
286
+ }
287
+
288
+ .btn:active {
289
+ box-shadow: none !important;
290
+ transform: translate(4px, 4px) !important;
291
+ }
292
+
293
+ /* 输入框 */
294
+ input,
295
+ textarea,
296
+ .form-control {
297
+ border: 3px solid var(--ai-border) !important;
298
+ border-radius: 0 !important;
299
+ background: var(--ai-bg-primary) !important;
300
+ color: var(--ai-text-primary) !important;
301
+ font-family: var(--ai-font-family) !important;
302
+ }
303
+
304
+ input:focus,
305
+ textarea:focus,
306
+ .form-control:focus {
307
+ outline: none !important;
308
+ border-color: var(--ai-primary) !important;
309
+ box-shadow: 4px 4px 0 var(--ai-bg-tertiary) !important;
310
+ }
311
+
312
+ /* 聊天气泡 */
313
+ .message-bubble {
314
+ border: 3px solid var(--ai-border) !important;
315
+ border-radius: 0 !important;
316
+ }
317
+
318
+ /* 标题 */
319
+ h2, h3, h4 {
320
+ font-family: var(--ai-font-family) !important;
321
+ letter-spacing: 1px !important;
322
+ }
323
+ }
324
+ `.trim();
325
+ }
326
+
327
+ if (theme === 'tech') {
328
+ return `
329
+ /* Tech theme specific styles */
330
+ ${this.containerSelectors.join(',\n')} {
331
+ /* 扫描线背景 */
332
+ background: repeating-linear-gradient(
333
+ 0deg,
334
+ transparent,
335
+ transparent 2px,
336
+ rgba(0, 255, 249, 0.03) 2px,
337
+ rgba(0, 255, 249, 0.03) 4px
338
+ ) !important;
339
+
340
+ /* 发光按钮 */
341
+ .btn {
342
+ background: linear-gradient(135deg, var(--ai-bg-secondary) 0%, var(--ai-bg-tertiary) 100%) !important;
343
+ border: 1px solid var(--ai-primary) !important;
344
+ color: var(--ai-text-primary) !important;
345
+ box-shadow:
346
+ 0 0 10px rgba(0, 255, 249, 0.2),
347
+ inset 0 0 10px rgba(0, 255, 249, 0.05) !important;
348
+ }
349
+
350
+ .btn:hover {
351
+ box-shadow:
352
+ 0 0 20px rgba(0, 255, 249, 0.4),
353
+ inset 0 0 20px rgba(0, 255, 249, 0.1) !important;
354
+ }
355
+
356
+ /* 发光输入框 */
357
+ input,
358
+ textarea,
359
+ .form-control {
360
+ border: 1px solid var(--ai-primary) !important;
361
+ background: var(--ai-bg-primary) !important;
362
+ color: var(--ai-text-primary) !important;
363
+ box-shadow: 0 0 10px rgba(0, 255, 249, 0.1) !important;
364
+ }
365
+
366
+ input:focus,
367
+ textarea:focus,
368
+ .form-control:focus {
369
+ outline: none !important;
370
+ border-color: var(--ai-primary) !important;
371
+ box-shadow:
372
+ 0 0 20px rgba(0, 255, 249, 0.3),
373
+ inset 0 0 10px rgba(0, 255, 249, 0.05) !important;
374
+ }
375
+
376
+ /* 霓虹标题 */
377
+ h2, h3, h4 {
378
+ text-shadow:
379
+ 0 0 10px var(--ai-primary),
380
+ 0 0 20px var(--ai-primary) !important;
381
+ }
382
+
383
+ /* 滚动条 */
384
+ ::-webkit-scrollbar {
385
+ width: 8px !important;
386
+ background: var(--ai-bg-secondary) !important;
387
+ }
388
+
389
+ ::-webkit-scrollbar-thumb {
390
+ background: var(--ai-primary) !important;
391
+ border-radius: 0 !important;
392
+ box-shadow: 0 0 10px var(--ai-primary) !important;
393
+ }
394
+ }
395
+ `.trim();
396
+ }
397
+
398
+ return '';
399
+ }
400
+
401
+ /**
402
+ * 更新 body 和 html 的类名
403
+ */
404
+ private updateBodyClasses(theme: ThemeType, effectiveTheme: ThemeType): void {
405
+ const root = document.documentElement;
406
+ const body = document.body;
407
+
408
+ // 移除所有主题类
409
+ this.allThemeClasses.forEach(cls => {
410
+ root.classList.remove(cls);
411
+ body.classList.remove(cls);
412
+ });
413
+
414
+ // 添加新主题类
415
+ const themeClass = `ai-theme-${theme}`;
416
+ const effectiveClass = `ai-theme-${effectiveTheme}`;
417
+
418
+ root.classList.add(themeClass);
419
+ body.classList.add(themeClass);
420
+ root.setAttribute('data-ai-theme', effectiveTheme);
421
+ body.setAttribute('data-ai-theme', effectiveTheme);
422
+
423
+ if (theme === 'auto') {
424
+ root.classList.add(effectiveClass);
425
+ body.classList.add(effectiveClass);
426
+ }
427
+
428
+ // 更新所有 AI 容器的类
429
+ const containers = document.querySelectorAll(this.containerSelectors.join(','));
430
+ containers.forEach(container => {
431
+ this.allThemeClasses.forEach(cls => container.classList.remove(cls));
432
+ container.classList.add(themeClass);
433
+ container.setAttribute('data-ai-theme', effectiveTheme);
434
+
435
+ if (theme === 'auto') {
436
+ container.classList.add(effectiveClass);
437
+ }
438
+ });
439
+ }
440
+
441
+ /**
442
+ * 获取 Tabby 当前的有效主题
443
+ */
444
+ private getTabbyEffectiveTheme(): 'light' | 'dark' {
445
+ const appearance = this.tabbyConfig.store?.appearance;
446
+
447
+ if (appearance) {
448
+ if (appearance.colorScheme) {
449
+ const scheme = appearance.colorScheme.toLowerCase();
450
+ if (scheme === 'light') return 'light';
451
+ if (scheme === 'dark') return 'dark';
452
+ }
453
+
454
+ const theme = appearance.theme?.toLowerCase() || '';
455
+ const darkThemes = ['hype', 'standard', 'dark', 'dracula', 'monokai', 'one-dark'];
456
+ if (darkThemes.some(t => theme.includes(t))) {
457
+ return 'dark';
458
+ }
459
+ }
460
+
461
+ return 'dark';
462
+ }
463
+
464
+ /**
465
+ * 刷新所有容器的主题类
466
+ */
467
+ refreshContainers(): void {
468
+ const theme = this.config.get<string>('theme', 'auto') as ThemeType;
469
+ this.applyTheme(theme);
470
+ }
471
+
472
+ ngOnDestroy(): void {
473
+ // 清理动态样式
474
+ if (this.styleElement && this.styleElement.parentNode) {
475
+ this.styleElement.parentNode.removeChild(this.styleElement);
476
+ }
477
+ this.tabbySubscription?.unsubscribe();
478
+ this.currentTheme$.complete();
479
+ }
480
+ }