tabby-ai-assistant 1.0.5 → 1.0.7

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 (116) hide show
  1. package/dist/components/chat/ai-sidebar.component.d.ts +147 -0
  2. package/dist/components/chat/chat-interface.component.d.ts +38 -6
  3. package/dist/components/settings/general-settings.component.d.ts +6 -3
  4. package/dist/components/settings/provider-config.component.d.ts +25 -12
  5. package/dist/components/terminal/command-preview.component.d.ts +38 -0
  6. package/dist/index-full.d.ts +8 -0
  7. package/dist/index-minimal.d.ts +3 -0
  8. package/dist/index.d.ts +7 -3
  9. package/dist/index.js +1 -2
  10. package/dist/providers/tabby/ai-config.provider.d.ts +57 -5
  11. package/dist/providers/tabby/ai-hotkey.provider.d.ts +8 -14
  12. package/dist/providers/tabby/ai-toolbar-button.provider.d.ts +8 -9
  13. package/dist/services/chat/ai-sidebar.service.d.ts +89 -0
  14. package/dist/services/chat/chat-history.service.d.ts +78 -0
  15. package/dist/services/chat/chat-session.service.d.ts +57 -2
  16. package/dist/services/context/compaction.d.ts +90 -0
  17. package/dist/services/context/manager.d.ts +69 -0
  18. package/dist/services/context/memory.d.ts +116 -0
  19. package/dist/services/context/token-budget.d.ts +105 -0
  20. package/dist/services/core/ai-assistant.service.d.ts +40 -1
  21. package/dist/services/core/checkpoint.service.d.ts +130 -0
  22. package/dist/services/platform/escape-sequence.service.d.ts +132 -0
  23. package/dist/services/platform/platform-detection.service.d.ts +146 -0
  24. package/dist/services/providers/anthropic-provider.service.d.ts +5 -0
  25. package/dist/services/providers/base-provider.service.d.ts +6 -1
  26. package/dist/services/providers/glm-provider.service.d.ts +5 -0
  27. package/dist/services/providers/minimax-provider.service.d.ts +10 -1
  28. package/dist/services/providers/ollama-provider.service.d.ts +76 -0
  29. package/dist/services/providers/openai-compatible.service.d.ts +5 -0
  30. package/dist/services/providers/openai-provider.service.d.ts +5 -0
  31. package/dist/services/providers/vllm-provider.service.d.ts +82 -0
  32. package/dist/services/terminal/buffer-analyzer.service.d.ts +128 -0
  33. package/dist/services/terminal/terminal-manager.service.d.ts +185 -0
  34. package/dist/services/terminal/terminal-tools.service.d.ts +79 -0
  35. package/dist/types/ai.types.d.ts +92 -0
  36. package/dist/types/provider.types.d.ts +1 -1
  37. package/package.json +8 -10
  38. package/src/components/chat/ai-sidebar.component.ts +945 -0
  39. package/src/components/chat/chat-input.component.html +9 -24
  40. package/src/components/chat/chat-input.component.scss +3 -2
  41. package/src/components/chat/chat-interface.component.html +77 -69
  42. package/src/components/chat/chat-interface.component.scss +54 -4
  43. package/src/components/chat/chat-interface.component.ts +250 -34
  44. package/src/components/chat/chat-settings.component.scss +4 -4
  45. package/src/components/chat/chat-settings.component.ts +22 -11
  46. package/src/components/common/error-message.component.html +15 -0
  47. package/src/components/common/error-message.component.scss +77 -0
  48. package/src/components/common/error-message.component.ts +2 -96
  49. package/src/components/common/loading-spinner.component.html +4 -0
  50. package/src/components/common/loading-spinner.component.scss +57 -0
  51. package/src/components/common/loading-spinner.component.ts +2 -63
  52. package/src/components/security/consent-dialog.component.html +22 -0
  53. package/src/components/security/consent-dialog.component.scss +34 -0
  54. package/src/components/security/consent-dialog.component.ts +2 -55
  55. package/src/components/security/password-prompt.component.html +19 -0
  56. package/src/components/security/password-prompt.component.scss +30 -0
  57. package/src/components/security/password-prompt.component.ts +2 -54
  58. package/src/components/security/risk-confirm-dialog.component.html +8 -12
  59. package/src/components/security/risk-confirm-dialog.component.scss +8 -5
  60. package/src/components/security/risk-confirm-dialog.component.ts +6 -6
  61. package/src/components/settings/ai-settings-tab.component.html +16 -20
  62. package/src/components/settings/ai-settings-tab.component.scss +8 -5
  63. package/src/components/settings/ai-settings-tab.component.ts +12 -12
  64. package/src/components/settings/general-settings.component.html +8 -17
  65. package/src/components/settings/general-settings.component.scss +6 -3
  66. package/src/components/settings/general-settings.component.ts +62 -22
  67. package/src/components/settings/provider-config.component.html +19 -39
  68. package/src/components/settings/provider-config.component.scss +182 -39
  69. package/src/components/settings/provider-config.component.ts +119 -7
  70. package/src/components/settings/security-settings.component.scss +1 -1
  71. package/src/components/terminal/ai-toolbar-button.component.html +8 -0
  72. package/src/components/terminal/ai-toolbar-button.component.scss +20 -0
  73. package/src/components/terminal/ai-toolbar-button.component.ts +2 -30
  74. package/src/components/terminal/command-preview.component.html +61 -0
  75. package/src/components/terminal/command-preview.component.scss +72 -0
  76. package/src/components/terminal/command-preview.component.ts +127 -140
  77. package/src/components/terminal/command-suggestion.component.html +23 -0
  78. package/src/components/terminal/command-suggestion.component.scss +55 -0
  79. package/src/components/terminal/command-suggestion.component.ts +2 -77
  80. package/src/index-minimal.ts +32 -0
  81. package/src/index.ts +94 -11
  82. package/src/index.ts.backup +165 -0
  83. package/src/providers/tabby/ai-config.provider.ts +60 -51
  84. package/src/providers/tabby/ai-hotkey.provider.ts +23 -39
  85. package/src/providers/tabby/ai-settings-tab.provider.ts +2 -2
  86. package/src/providers/tabby/ai-toolbar-button.provider.ts +29 -24
  87. package/src/services/chat/ai-sidebar.service.ts +258 -0
  88. package/src/services/chat/chat-history.service.ts +308 -0
  89. package/src/services/chat/chat-history.service.ts.backup +239 -0
  90. package/src/services/chat/chat-session.service.ts +276 -3
  91. package/src/services/context/compaction.ts +483 -0
  92. package/src/services/context/manager.ts +442 -0
  93. package/src/services/context/memory.ts +519 -0
  94. package/src/services/context/token-budget.ts +422 -0
  95. package/src/services/core/ai-assistant.service.ts +280 -5
  96. package/src/services/core/ai-provider-manager.service.ts +2 -2
  97. package/src/services/core/checkpoint.service.ts +619 -0
  98. package/src/services/platform/escape-sequence.service.ts +499 -0
  99. package/src/services/platform/platform-detection.service.ts +494 -0
  100. package/src/services/providers/anthropic-provider.service.ts +28 -1
  101. package/src/services/providers/base-provider.service.ts +7 -1
  102. package/src/services/providers/glm-provider.service.ts +28 -1
  103. package/src/services/providers/minimax-provider.service.ts +209 -11
  104. package/src/services/providers/ollama-provider.service.ts +445 -0
  105. package/src/services/providers/openai-compatible.service.ts +9 -0
  106. package/src/services/providers/openai-provider.service.ts +9 -0
  107. package/src/services/providers/vllm-provider.service.ts +463 -0
  108. package/src/services/security/risk-assessment.service.ts +6 -2
  109. package/src/services/terminal/buffer-analyzer.service.ts +594 -0
  110. package/src/services/terminal/terminal-manager.service.ts +748 -0
  111. package/src/services/terminal/terminal-tools.service.ts +441 -0
  112. package/src/styles/ai-assistant.scss +78 -6
  113. package/src/types/ai.types.ts +144 -0
  114. package/src/types/provider.types.ts +1 -1
  115. package/tsconfig.json +9 -9
  116. package/webpack.config.js +28 -6
@@ -0,0 +1,258 @@
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
+ }
15
+
16
+ /**
17
+ * AI Sidebar 服务 - 管理 AI 聊天侧边栏的生命周期
18
+ *
19
+ * 采用 Flexbox 布局方式,将 sidebar 插入到 app-root 作为第一个子元素,
20
+ * app-root 变为水平 flex 容器,sidebar 在左侧
21
+ */
22
+ @Injectable({ providedIn: 'root' })
23
+ export class AiSidebarService {
24
+ private sidebarComponentRef: ComponentRef<AiSidebarComponent> | null = null;
25
+ private sidebarElement: HTMLElement | null = null;
26
+ private styleElement: HTMLStyleElement | null = null;
27
+ private _isVisible = false;
28
+ private readonly SIDEBAR_WIDTH = 320;
29
+
30
+ /**
31
+ * 侧边栏是否可见
32
+ */
33
+ get sidebarVisible(): boolean {
34
+ return this._isVisible;
35
+ }
36
+
37
+ constructor(
38
+ private componentFactoryResolver: ComponentFactoryResolver,
39
+ private appRef: ApplicationRef,
40
+ private injector: Injector,
41
+ private config: ConfigService,
42
+ ) { }
43
+
44
+ /**
45
+ * 显示 sidebar
46
+ */
47
+ show(): void {
48
+ if (this._isVisible) {
49
+ return;
50
+ }
51
+
52
+ this.createSidebar();
53
+
54
+ const pluginConfig = this.getPluginConfig();
55
+ pluginConfig.sidebarVisible = true;
56
+ this.savePluginConfig(pluginConfig);
57
+
58
+ this._isVisible = true;
59
+ }
60
+
61
+ /**
62
+ * 隐藏 sidebar
63
+ */
64
+ hide(): void {
65
+ if (!this._isVisible) {
66
+ return;
67
+ }
68
+
69
+ this.destroySidebar();
70
+
71
+ const pluginConfig = this.getPluginConfig();
72
+ pluginConfig.sidebarVisible = false;
73
+ this.savePluginConfig(pluginConfig);
74
+
75
+ this._isVisible = false;
76
+ }
77
+
78
+ /**
79
+ * 切换 sidebar 显示状态
80
+ */
81
+ toggle(): void {
82
+ if (this._isVisible) {
83
+ this.hide();
84
+ } else {
85
+ this.show();
86
+ }
87
+ }
88
+
89
+ /**
90
+ * 获取当前显示状态
91
+ */
92
+ get visible(): boolean {
93
+ return this._isVisible;
94
+ }
95
+
96
+ /**
97
+ * 初始化 - 应用启动时调用
98
+ */
99
+ initialize(): void {
100
+ const pluginConfig = this.getPluginConfig();
101
+ // 默认显示 sidebar,除非明确设置为隐藏
102
+ if (pluginConfig.sidebarVisible !== false) {
103
+ this.show();
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 创建 sidebar 组件
109
+ *
110
+ * 使用固定定位方案:
111
+ * 1. 侧边栏 position: fixed,固定在左侧
112
+ * 2. 主内容区通过 margin-left 推开
113
+ * 这样不改变任何现有元素的 flex 布局
114
+ */
115
+ private createSidebar(): void {
116
+ // 创建组件
117
+ const componentFactory = this.componentFactoryResolver.resolveComponentFactory(AiSidebarComponent);
118
+ this.sidebarComponentRef = componentFactory.create(this.injector);
119
+
120
+ // 附加到应用
121
+ this.appRef.attachView(this.sidebarComponentRef.hostView);
122
+
123
+ // 获取 DOM 元素
124
+ const domElem = (this.sidebarComponentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
125
+
126
+ // 创建 wrapper 元素 - 使用固定定位
127
+ const wrapper = document.createElement('div');
128
+ wrapper.className = 'ai-sidebar-wrapper';
129
+ wrapper.style.cssText = `
130
+ position: fixed;
131
+ left: 0;
132
+ top: 0;
133
+ width: ${this.SIDEBAR_WIDTH}px;
134
+ height: 100%;
135
+ display: flex;
136
+ flex-direction: column;
137
+ background: var(--bs-body-bg, #1e1e1e);
138
+ border-right: 1px solid var(--bs-border-color, #333);
139
+ box-shadow: 2px 0 10px rgba(0,0,0,0.3);
140
+ z-index: 1000;
141
+ `;
142
+
143
+ wrapper.appendChild(domElem);
144
+
145
+ // 插入到 body
146
+ document.body.appendChild(wrapper);
147
+
148
+ this.sidebarElement = wrapper;
149
+
150
+ // 注入布局 CSS - 只添加 margin-left 把主内容推开
151
+ this.injectLayoutCSS();
152
+
153
+ // 注入服务引用到组件
154
+ if (this.sidebarComponentRef) {
155
+ const component = this.sidebarComponentRef.instance;
156
+ component.sidebarService = this;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * 销毁 sidebar 组件
162
+ */
163
+ private destroySidebar(): void {
164
+ // 移除注入的 CSS
165
+ this.removeLayoutCSS();
166
+
167
+ if (this.sidebarComponentRef) {
168
+ this.appRef.detachView(this.sidebarComponentRef.hostView);
169
+ this.sidebarComponentRef.destroy();
170
+ this.sidebarComponentRef = null;
171
+ }
172
+
173
+ if (this.sidebarElement) {
174
+ this.sidebarElement.remove();
175
+ this.sidebarElement = null;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * 调整 .content 元素样式 - 只处理第二个(更深层的).content
181
+ */
182
+ private adjustContentStyles(appRoot: Element, apply: boolean): void {
183
+ const contentElements = appRoot.querySelectorAll('.content');
184
+
185
+ if (contentElements.length > 1) {
186
+ // 选择第二个(更深层的).content 元素,这是 Tabby 的主内容区
187
+ const contentElement = contentElements[1] as HTMLElement;
188
+ if (apply) {
189
+ contentElement.style.width = 'auto';
190
+ contentElement.style.flex = '1 1 auto';
191
+ contentElement.style.minWidth = '0';
192
+ } else {
193
+ contentElement.style.removeProperty('width');
194
+ contentElement.style.removeProperty('flex');
195
+ contentElement.style.removeProperty('min-width');
196
+ }
197
+ } else if (contentElements.length === 1) {
198
+ // 如果只有一个 .content,则处理它
199
+ const contentElement = contentElements[0] as HTMLElement;
200
+ if (apply) {
201
+ contentElement.style.width = 'auto';
202
+ contentElement.style.flex = '1 1 auto';
203
+ contentElement.style.minWidth = '0';
204
+ } else {
205
+ contentElement.style.removeProperty('width');
206
+ contentElement.style.removeProperty('flex');
207
+ contentElement.style.removeProperty('min-width');
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * 注入布局 CSS - 使用 margin-left 把主内容推开
214
+ *
215
+ * 固定定位方案:侧边栏 fixed,主内容区 margin-left
216
+ */
217
+ private injectLayoutCSS(): void {
218
+ const style = document.createElement('style');
219
+ style.id = 'ai-sidebar-layout-css';
220
+ style.textContent = `
221
+ /* 用 margin-left 把 app-root 推开,为侧边栏腾出空间 */
222
+ app-root {
223
+ margin-left: ${this.SIDEBAR_WIDTH}px !important;
224
+ }
225
+ `;
226
+
227
+ document.head.appendChild(style);
228
+ this.styleElement = style;
229
+ }
230
+
231
+ /**
232
+ * 移除布局 CSS
233
+ */
234
+ private removeLayoutCSS(): void {
235
+ if (this.styleElement) {
236
+ this.styleElement.remove();
237
+ this.styleElement = null;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * 获取插件配置
243
+ */
244
+ private getPluginConfig(): AiSidebarConfig {
245
+ return this.config.store.pluginConfig?.['ai-assistant'] || {};
246
+ }
247
+
248
+ /**
249
+ * 保存插件配置
250
+ */
251
+ private savePluginConfig(pluginConfig: AiSidebarConfig): void {
252
+ if (!this.config.store.pluginConfig) {
253
+ this.config.store.pluginConfig = {};
254
+ }
255
+ this.config.store.pluginConfig['ai-assistant'] = pluginConfig;
256
+ this.config.save();
257
+ }
258
+ }
@@ -10,6 +10,23 @@ export interface SavedSession {
10
10
  createdAt: Date;
11
11
  updatedAt: Date;
12
12
  messageCount: number;
13
+ // 上下文工程相关字段
14
+ contextInfo?: {
15
+ tokenUsage?: {
16
+ input: number;
17
+ output: number;
18
+ cacheRead: number;
19
+ cacheWrite: number;
20
+ };
21
+ compactionHistory?: Array<{
22
+ timestamp: Date;
23
+ type: 'prune' | 'compact' | 'truncate';
24
+ tokensSaved: number;
25
+ condenseId?: string;
26
+ }>;
27
+ hasCompression?: boolean;
28
+ lastCompactionAt?: Date;
29
+ };
13
30
  }
14
31
 
15
32
  const STORAGE_KEY = 'tabby-ai-assistant-chat-history';
@@ -236,4 +253,295 @@ export class ChatHistoryService {
236
253
  // 保留最近的messages
237
254
  return messages.slice(-MAX_MESSAGES_PER_SESSION);
238
255
  }
256
+
257
+ /**
258
+ * 更新会话的上下文信息(压缩标记支持)
259
+ */
260
+ updateContextInfo(sessionId: string, contextInfo: SavedSession['contextInfo']): void {
261
+ const sessions = this.sessionsSubject.value;
262
+ const existingIndex = sessions.findIndex(s => s.sessionId === sessionId);
263
+
264
+ if (existingIndex >= 0) {
265
+ sessions[existingIndex] = {
266
+ ...sessions[existingIndex],
267
+ contextInfo
268
+ };
269
+ this.sessionsSubject.next([...sessions]);
270
+ this.saveToStorage(sessions);
271
+ this.logger.info('Context info updated', { sessionId });
272
+ }
273
+ }
274
+
275
+ /**
276
+ * 导出会话(包含压缩状态)
277
+ */
278
+ exportSessionWithContext(sessionId: string): string | undefined {
279
+ const session = this.loadSession(sessionId);
280
+ if (!session) {
281
+ return undefined;
282
+ }
283
+
284
+ const exportData = {
285
+ exportDate: new Date().toISOString(),
286
+ version: '2.0',
287
+ session: {
288
+ ...session,
289
+ contextInfo: session.contextInfo || {}
290
+ }
291
+ };
292
+ return JSON.stringify(exportData, null, 2);
293
+ }
294
+
295
+ /**
296
+ * 导出会话历史(包含所有压缩状态)
297
+ */
298
+ exportAllHistoryWithContext(): string {
299
+ const sessions = this.sessionsSubject.value;
300
+ const exportData = {
301
+ exportDate: new Date().toISOString(),
302
+ version: '2.0',
303
+ sessions: sessions.map(s => ({
304
+ ...s,
305
+ contextInfo: s.contextInfo || {}
306
+ }))
307
+ };
308
+ return JSON.stringify(exportData, null, 2);
309
+ }
310
+
311
+ /**
312
+ * 导入会话(包含压缩状态)
313
+ */
314
+ importSessionWithContext(data: string): void {
315
+ try {
316
+ const importData = JSON.parse(data);
317
+
318
+ if (!importData.session && !importData.sessions) {
319
+ throw new Error('Invalid session format');
320
+ }
321
+
322
+ const sessions = this.sessionsSubject.value;
323
+
324
+ if (importData.session) {
325
+ // 导入单个会话
326
+ const session = importData.session;
327
+ const existingIndex = sessions.findIndex(s => s.sessionId === session.sessionId);
328
+
329
+ const processedSession = {
330
+ ...session,
331
+ createdAt: new Date(session.createdAt),
332
+ updatedAt: new Date(session.updatedAt),
333
+ contextInfo: session.contextInfo || undefined
334
+ };
335
+
336
+ if (existingIndex >= 0) {
337
+ sessions[existingIndex] = processedSession;
338
+ } else {
339
+ sessions.unshift(processedSession);
340
+ }
341
+ } else if (importData.sessions) {
342
+ // 批量导入会话
343
+ const importedSessions = importData.sessions.map((s: any) => ({
344
+ ...s,
345
+ createdAt: new Date(s.createdAt),
346
+ updatedAt: new Date(s.updatedAt),
347
+ contextInfo: s.contextInfo || undefined
348
+ }));
349
+
350
+ // 合并现有会话,避免重复
351
+ const mergedSessions = [...sessions];
352
+ importedSessions.forEach(imported => {
353
+ const existingIndex = mergedSessions.findIndex(s => s.sessionId === imported.sessionId);
354
+ if (existingIndex >= 0) {
355
+ mergedSessions[existingIndex] = imported;
356
+ } else {
357
+ mergedSessions.unshift(imported);
358
+ }
359
+ });
360
+
361
+ // 限制会话数量
362
+ this.sessionsSubject.next(mergedSessions.slice(0, MAX_SESSIONS));
363
+ this.saveToStorage(mergedSessions.slice(0, MAX_SESSIONS));
364
+ }
365
+
366
+ this.logger.info('Session(s) with context imported successfully');
367
+ } catch (error) {
368
+ this.logger.error('Failed to import session with context', error);
369
+ throw new Error('Invalid session file format');
370
+ }
371
+ }
372
+
373
+ /**
374
+ * 清理压缩数据
375
+ */
376
+ cleanupCompressedData(sessionId?: string): void {
377
+ const sessions = this.sessionsSubject.value;
378
+
379
+ if (sessionId) {
380
+ // 清理指定会话的压缩数据
381
+ const sessionIndex = sessions.findIndex(s => s.sessionId === sessionId);
382
+ if (sessionIndex >= 0 && sessions[sessionIndex].contextInfo) {
383
+ sessions[sessionIndex] = {
384
+ ...sessions[sessionIndex],
385
+ contextInfo: undefined
386
+ };
387
+ this.sessionsSubject.next([...sessions]);
388
+ this.saveToStorage(sessions);
389
+ this.logger.info('Compressed data cleaned for session', { sessionId });
390
+ }
391
+ } else {
392
+ // 清理所有会话的压缩数据
393
+ sessions.forEach(session => {
394
+ if (session.contextInfo) {
395
+ session.contextInfo = undefined;
396
+ }
397
+ });
398
+ this.sessionsSubject.next([...sessions]);
399
+ this.saveToStorage(sessions);
400
+ this.logger.info('All compressed data cleaned');
401
+ }
402
+ }
403
+
404
+ /**
405
+ * 获取压缩统计信息
406
+ */
407
+ getCompressionStatistics(): {
408
+ totalSessions: number;
409
+ sessionsWithCompression: number;
410
+ totalTokensSaved: number;
411
+ averageTokensSaved: number;
412
+ lastCompactionAt?: Date;
413
+ compactionHistory: Array<{
414
+ sessionId: string;
415
+ type: 'prune' | 'compact' | 'truncate';
416
+ timestamp: Date;
417
+ tokensSaved: number;
418
+ }>;
419
+ } {
420
+ const sessions = this.sessionsSubject.value;
421
+ const sessionsWithCompression = sessions.filter(s => s.contextInfo?.hasCompression);
422
+ let totalTokensSaved = 0;
423
+ const allCompactionEvents: Array<{
424
+ sessionId: string;
425
+ type: 'prune' | 'compact' | 'truncate';
426
+ timestamp: Date;
427
+ tokensSaved: number;
428
+ }> = [];
429
+ let lastCompactionAt: Date | undefined;
430
+
431
+ sessionsWithCompression.forEach(session => {
432
+ if (session.contextInfo?.compactionHistory) {
433
+ session.contextInfo.compactionHistory.forEach(event => {
434
+ totalTokensSaved += event.tokensSaved;
435
+ allCompactionEvents.push({
436
+ sessionId: session.sessionId,
437
+ type: event.type,
438
+ timestamp: event.timestamp,
439
+ tokensSaved: event.tokensSaved
440
+ });
441
+
442
+ if (!lastCompactionAt || event.timestamp > lastCompactionAt) {
443
+ lastCompactionAt = event.timestamp;
444
+ }
445
+ });
446
+ }
447
+ });
448
+
449
+ return {
450
+ totalSessions: sessions.length,
451
+ sessionsWithCompression: sessionsWithCompression.length,
452
+ totalTokensSaved,
453
+ averageTokensSaved: sessionsWithCompression.length > 0
454
+ ? Math.round(totalTokensSaved / sessionsWithCompression.length)
455
+ : 0,
456
+ lastCompactionAt,
457
+ compactionHistory: allCompactionEvents
458
+ };
459
+ }
460
+
461
+ /**
462
+ * 记录压缩事件
463
+ */
464
+ recordCompactionEvent(
465
+ sessionId: string,
466
+ type: 'prune' | 'compact' | 'truncate',
467
+ tokensSaved: number,
468
+ condenseId?: string
469
+ ): void {
470
+ const sessions = this.sessionsSubject.value;
471
+ const sessionIndex = sessions.findIndex(s => s.sessionId === sessionId);
472
+
473
+ if (sessionIndex >= 0) {
474
+ const session = sessions[sessionIndex];
475
+ const contextInfo = session.contextInfo || {};
476
+
477
+ const compactionHistory = contextInfo.compactionHistory || [];
478
+ compactionHistory.push({
479
+ timestamp: new Date(),
480
+ type,
481
+ tokensSaved,
482
+ condenseId
483
+ });
484
+
485
+ sessions[sessionIndex] = {
486
+ ...session,
487
+ contextInfo: {
488
+ ...contextInfo,
489
+ hasCompression: true,
490
+ lastCompactionAt: new Date(),
491
+ compactionHistory
492
+ }
493
+ };
494
+
495
+ this.sessionsSubject.next([...sessions]);
496
+ this.saveToStorage(sessions);
497
+ this.logger.info('Compaction event recorded', { sessionId, type, tokensSaved });
498
+ }
499
+ }
500
+
501
+ /**
502
+ * 检查会话是否有压缩标记
503
+ */
504
+ hasCompressionMarkers(sessionId: string): boolean {
505
+ const session = this.loadSession(sessionId);
506
+ return session?.contextInfo?.hasCompression || false;
507
+ }
508
+
509
+ /**
510
+ * 获取会话的压缩历史
511
+ */
512
+ getCompactionHistory(sessionId: string): Array<{
513
+ timestamp: Date;
514
+ type: 'prune' | 'compact' | 'truncate';
515
+ tokensSaved: number;
516
+ condenseId?: string;
517
+ }> {
518
+ const session = this.loadSession(sessionId);
519
+ return session?.contextInfo?.compactionHistory || [];
520
+ }
521
+
522
+ /**
523
+ * 更新Token使用统计
524
+ */
525
+ updateTokenUsage(sessionId: string, tokenUsage?: {
526
+ input: number;
527
+ output: number;
528
+ cacheRead: number;
529
+ cacheWrite: number;
530
+ }): void {
531
+ const sessions = this.sessionsSubject.value;
532
+ const sessionIndex = sessions.findIndex(s => s.sessionId === sessionId);
533
+
534
+ if (sessionIndex >= 0) {
535
+ const session = sessions[sessionIndex];
536
+ sessions[sessionIndex] = {
537
+ ...session,
538
+ contextInfo: {
539
+ ...session.contextInfo,
540
+ tokenUsage
541
+ }
542
+ };
543
+ this.sessionsSubject.next([...sessions]);
544
+ this.saveToStorage(sessions);
545
+ }
546
+ }
239
547
  }