diff --git a/packages/ai-core/src/common/communication-recording-service.ts b/packages/ai-core/src/common/communication-recording-service.ts index 491d8065173e5..4ae26c47e0e2e 100644 --- a/packages/ai-core/src/common/communication-recording-service.ts +++ b/packages/ai-core/src/common/communication-recording-service.ts @@ -41,4 +41,7 @@ export interface CommunicationRecordingService { readonly onDidRecordResponse: Event; getHistory(agentId: string): CommunicationHistory; + + clearHistory(): void; + readonly onStructuralChange: Event; } diff --git a/packages/ai-history/src/browser/ai-history-contribution.ts b/packages/ai-history/src/browser/ai-history-contribution.ts index f33d71cb6793d..38d87ce4aa425 100644 --- a/packages/ai-history/src/browser/ai-history-contribution.ts +++ b/packages/ai-history/src/browser/ai-history-contribution.ts @@ -13,11 +13,13 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { FrontendApplication } from '@theia/core/lib/browser'; +import { FrontendApplication, codicon } from '@theia/core/lib/browser'; import { AIViewContribution } from '@theia/ai-core/lib/browser'; -import { injectable } from '@theia/core/shared/inversify'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { AIHistoryView } from './ai-history-widget'; -import { Command, CommandRegistry } from '@theia/core'; +import { Command, CommandRegistry, Emitter } from '@theia/core'; +import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { CommunicationRecordingService } from '@theia/ai-core'; export const AI_HISTORY_TOGGLE_COMMAND_ID = 'aiHistory:toggle'; export const OPEN_AI_HISTORY_VIEW = Command.toLocalizedCommand({ @@ -25,8 +27,28 @@ export const OPEN_AI_HISTORY_VIEW = Command.toLocalizedCommand({ label: 'Open AI History view', }); +export const AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY = Command.toLocalizedCommand({ + id: 'aiHistory:sortChronologically', + label: 'AI History: Sort chronologically', + iconClass: codicon('arrow-down') +}); + +export const AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY = Command.toLocalizedCommand({ + id: 'aiHistory:sortReverseChronologically', + label: 'AI History: Sort reverse chronologically', + iconClass: codicon('arrow-up') +}); + +export const AI_HISTORY_VIEW_CLEAR = Command.toLocalizedCommand({ + id: 'aiHistory:clear', + label: 'AI History: Clear History', + iconClass: codicon('clear-all') +}); + @injectable() -export class AIHistoryViewContribution extends AIViewContribution { +export class AIHistoryViewContribution extends AIViewContribution implements TabBarToolbarContribution { + @inject(CommunicationRecordingService) private recordingService: CommunicationRecordingService; + constructor() { super({ widgetId: AIHistoryView.ID, @@ -43,10 +65,78 @@ export class AIHistoryViewContribution extends AIViewContribution await this.openView(); } - override registerCommands(commands: CommandRegistry): void { - super.registerCommands(commands); - commands.registerCommand(OPEN_AI_HISTORY_VIEW, { + override registerCommands(registry: CommandRegistry): void { + super.registerCommands(registry); + registry.registerCommand(OPEN_AI_HISTORY_VIEW, { execute: () => this.openView({ activate: true }), }); + registry.registerCommand(AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY, { + isEnabled: widget => this.withHistoryWidget(widget, historyView => !historyView.isChronological), + isVisible: widget => this.withHistoryWidget(widget, historyView => !historyView.isChronological), + execute: widget => this.withHistoryWidget(widget, historyView => { + historyView.sortHistory(true); + return true; + }) + }); + registry.registerCommand(AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY, { + isEnabled: widget => this.withHistoryWidget(widget, historyView => historyView.isChronological), + isVisible: widget => this.withHistoryWidget(widget, historyView => historyView.isChronological), + execute: widget => this.withHistoryWidget(widget, historyView => { + historyView.sortHistory(false); + return true; + }) + }); + registry.registerCommand(AI_HISTORY_VIEW_CLEAR, { + isEnabled: widget => this.withHistoryWidget(widget), + isVisible: widget => this.withHistoryWidget(widget), + execute: widget => this.withHistoryWidget(widget, () => { + this.clearHistory(); + return true; + }) + }); + } + public clearHistory(): void { + this.recordingService.clearHistory(); + } + + protected withHistoryWidget( + widget: unknown = this.tryGetWidget(), + predicate: (output: AIHistoryView) => boolean = () => true + ): boolean | false { + return widget instanceof AIHistoryView ? predicate(widget) : false; + } + + protected readonly onAIHistoryWidgetStateChangedEmitter = new Emitter(); + protected readonly onAIHistoryWidgettStateChanged = this.onAIHistoryWidgetStateChangedEmitter.event; + + @postConstruct() + protected override init(): void { + super.init(); + this.widget.then(widget => { + widget.onStateChanged(() => this.onAIHistoryWidgetStateChangedEmitter.fire()); + }); + } + + registerToolbarItems(registry: TabBarToolbarRegistry): void { + registry.registerItem({ + id: AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY.id, + command: AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY.id, + tooltip: 'Sort chronologically', + isVisible: widget => this.withHistoryWidget(widget), + onDidChange: this.onAIHistoryWidgettStateChanged + }); + registry.registerItem({ + id: AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY.id, + command: AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY.id, + tooltip: 'Sort reverse chronologically', + isVisible: widget => this.withHistoryWidget(widget), + onDidChange: this.onAIHistoryWidgettStateChanged + }); + registry.registerItem({ + id: AI_HISTORY_VIEW_CLEAR.id, + command: AI_HISTORY_VIEW_CLEAR.id, + tooltip: 'Clear History of all agents', + isVisible: widget => this.withHistoryWidget(widget) + }); } } diff --git a/packages/ai-history/src/browser/ai-history-frontend-module.ts b/packages/ai-history/src/browser/ai-history-frontend-module.ts index 021fc013cabdd..460dc9a6a06a6 100644 --- a/packages/ai-history/src/browser/ai-history-frontend-module.ts +++ b/packages/ai-history/src/browser/ai-history-frontend-module.ts @@ -21,6 +21,7 @@ import { ILogger } from '@theia/core'; import { AIHistoryViewContribution } from './ai-history-contribution'; import { AIHistoryView } from './ai-history-widget'; import '../../src/browser/style/ai-history.css'; +import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; export default new ContainerModule(bind => { bind(DefaultCommunicationRecordingService).toSelf().inSingletonScope(); @@ -38,4 +39,6 @@ export default new ContainerModule(bind => { id: AIHistoryView.ID, createWidget: () => context.container.get(AIHistoryView) })).inSingletonScope(); + bind(TabBarToolbarContribution).toService(AIHistoryViewContribution); + }); diff --git a/packages/ai-history/src/browser/ai-history-widget.tsx b/packages/ai-history/src/browser/ai-history-widget.tsx index 28277426f31a1..938b92f5e1ee2 100644 --- a/packages/ai-history/src/browser/ai-history-widget.tsx +++ b/packages/ai-history/src/browser/ai-history-widget.tsx @@ -14,14 +14,21 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { Agent, AgentService, CommunicationRecordingService, CommunicationRequestEntry, CommunicationResponseEntry } from '@theia/ai-core'; -import { codicon, ReactWidget } from '@theia/core/lib/browser'; +import { codicon, ReactWidget, StatefulWidget } from '@theia/core/lib/browser'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; import { CommunicationCard } from './ai-history-communication-card'; import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component'; +import { deepClone, Emitter } from '@theia/core'; + +namespace AIHistoryView { + export interface State { + chronological: boolean; + } +} @injectable() -export class AIHistoryView extends ReactWidget { +export class AIHistoryView extends ReactWidget implements StatefulWidget { @inject(CommunicationRecordingService) protected recordingService: CommunicationRecordingService; @inject(AgentService) @@ -32,6 +39,10 @@ export class AIHistoryView extends ReactWidget { protected selectedAgent?: Agent; + protected _state: AIHistoryView.State = { chronological: false }; + protected readonly onStateChangedEmitter = new Emitter(); + readonly onStateChanged = this.onStateChangedEmitter.event; + constructor() { super(); this.id = AIHistoryView.ID; @@ -41,11 +52,34 @@ export class AIHistoryView extends ReactWidget { this.title.iconClass = codicon('history'); } + protected get state(): AIHistoryView.State { + return this._state; + } + + protected set state(state: AIHistoryView.State) { + this._state = state; + this.onStateChangedEmitter.fire(this._state); + } + + storeState(): object { + return this.state; + } + + restoreState(oldState: object & Partial): void { + const copy = deepClone(this.state); + if (oldState.chronological) { + copy.chronological = oldState.chronological; + } + this.state = copy; + } + @postConstruct() protected init(): void { this.update(); this.toDispose.push(this.recordingService.onDidRecordRequest(entry => this.historyContentUpdated(entry))); this.toDispose.push(this.recordingService.onDidRecordResponse(entry => this.historyContentUpdated(entry))); + this.toDispose.push(this.recordingService.onStructuralChange(() => this.update())); + this.toDispose.push(this.onStateChanged(newState => this.update())); this.selectAgent(this.agentService.getAllAgents()[0]); } @@ -82,10 +116,13 @@ export class AIHistoryView extends ReactWidget { if (!this.selectedAgent) { return
No agent selected.
; } - const history = this.recordingService.getHistory(this.selectedAgent.id); + const history = [...this.recordingService.getHistory(this.selectedAgent.id)]; if (history.length === 0) { return
No history available for the selected agent '{this.selectedAgent.name}'.
; } + if (!this.state.chronological) { + history.reverse(); + } return history.map(entry => ); } @@ -93,4 +130,12 @@ export class AIHistoryView extends ReactWidget { e.stopPropagation(); this.selectAgent(agent); } + + public sortHistory(chronological: boolean): void { + this.state = { ...deepClone(this.state), chronological: chronological }; + } + + get isChronological(): boolean { + return this.state.chronological === true; + } } diff --git a/packages/ai-history/src/common/communication-recording-service.ts b/packages/ai-history/src/common/communication-recording-service.ts index 9d23a6766064e..d32eb6ffc9121 100644 --- a/packages/ai-history/src/common/communication-recording-service.ts +++ b/packages/ai-history/src/common/communication-recording-service.ts @@ -29,6 +29,9 @@ export class DefaultCommunicationRecordingService implements CommunicationRecord protected onDidRecordResponseEmitter = new Emitter(); readonly onDidRecordResponse: Event = this.onDidRecordResponseEmitter.event; + protected onStructuralChangeEmitter = new Emitter(); + readonly onStructuralChange: Event = this.onStructuralChangeEmitter.event; + protected history: Map = new Map(); getHistory(agentId: string): CommunicationHistory { @@ -60,4 +63,9 @@ export class DefaultCommunicationRecordingService implements CommunicationRecord } } } + + clearHistory(): void { + this.history.clear(); + this.onStructuralChangeEmitter.fire(undefined); + } }