From ecba0289c0e97d4b01c30b7c062645c593e2ca98 Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Sun, 29 Sep 2024 23:40:07 +0200 Subject: [PATCH 1/3] Allow to order and clear AI History view fixed #14183 Signed-off-by: Jonas Helming --- .../common/communication-recording-service.ts | 3 + .../src/browser/ai-history-contribution.ts | 109 ++++++++++++++++-- .../src/browser/ai-history-frontend-module.ts | 3 + .../src/browser/ai-history-widget.tsx | 54 ++++++++- .../common/communication-recording-service.ts | 8 ++ 5 files changed, 166 insertions(+), 11 deletions(-) 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..df10f7ee3f6b7 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, Widget, 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,9 +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 { - constructor() { +export class AIHistoryViewContribution extends AIViewContribution implements TabBarToolbarContribution { + recordingService: CommunicationRecordingService; + constructor(@inject(CommunicationRecordingService) recordingService: CommunicationRecordingService) { super({ widgetId: AIHistoryView.ID, widgetName: AIHistoryView.LABEL, @@ -37,16 +58,88 @@ export class AIHistoryViewContribution extends AIViewContribution }, toggleCommandId: AI_HISTORY_TOGGLE_COMMAND_ID, }); + this.recordingService = recordingService; } async initializeLayout(_app: FrontendApplication): Promise { 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.withWidget(widget, () => !widget.isChronologial), + isVisible: widget => this.withWidget(widget, () => !widget.isChronologial), + execute: widget => this.withWidget(widget, chatWidget => { + widget.sortHistory(true); + return true; + }) + }); + registry.registerCommand(AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY, { + isEnabled: widget => this.withWidget(widget, () => widget.isChronologial), + isVisible: widget => this.withWidget(widget, () => widget.isChronologial), + execute: widget => this.withWidget(widget, chatWidget => { + widget.sortHistory(false); + return true; + }) + }); + registry.registerCommand(AI_HISTORY_VIEW_CLEAR, { + isEnabled: widget => this.withWidget(widget, () => true), + isVisible: widget => this.withWidget(widget, () => true), + execute: widget => this.withWidget(widget, chatWidget => { + this.clearHistory(); + return true; + }) + }); + } + public clearHistory(): void { + this.recordingService.clearHistory(); + } + + protected withWidget( + widget: Widget | undefined = 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.isHistoryViewWidget(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.isHistoryViewWidget(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.isHistoryViewWidget(widget) + }); + } + protected isHistoryViewWidget(widget?: Widget): boolean { + return !!widget && AIHistoryView.ID === widget.id; } } 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..b984f00b750f6 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, Event } from '@theia/core'; + +export 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,9 @@ export class AIHistoryView extends ReactWidget { protected selectedAgent?: Agent; + protected _state: AIHistoryView.State = { chronological: false }; + protected readonly onStateChangedEmitter = new Emitter(); + constructor() { super(); this.id = AIHistoryView.ID; @@ -41,11 +51,38 @@ 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); + } + + get onStateChanged(): Event { + return this.onStateChangedEmitter.event; + } + + 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 +119,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 +133,12 @@ export class AIHistoryView extends ReactWidget { e.stopPropagation(); this.selectAgent(agent); } + + public sortHistory(chronological: boolean): void { + this.state = { ...deepClone(this.state), chronological: chronological }; + } + + get isChronologial(): boolean { + return !!this.state.chronological; + } } 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); + } } From f6f81ebb96071755c7e125bdaf67e5a56ad60219 Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Tue, 1 Oct 2024 16:52:18 +0200 Subject: [PATCH 2/3] Address review comments Signed-off-by: Jonas Helming --- .../src/browser/ai-history-contribution.ts | 43 +++++++++---------- .../src/browser/ai-history-widget.tsx | 11 ++--- 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/packages/ai-history/src/browser/ai-history-contribution.ts b/packages/ai-history/src/browser/ai-history-contribution.ts index df10f7ee3f6b7..dfdb808649e7f 100644 --- a/packages/ai-history/src/browser/ai-history-contribution.ts +++ b/packages/ai-history/src/browser/ai-history-contribution.ts @@ -13,7 +13,7 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { FrontendApplication, Widget, codicon } from '@theia/core/lib/browser'; +import { FrontendApplication, codicon } from '@theia/core/lib/browser'; import { AIViewContribution } from '@theia/ai-core/lib/browser'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { AIHistoryView } from './ai-history-widget'; @@ -47,8 +47,9 @@ export const AI_HISTORY_VIEW_CLEAR = Command.toLocalizedCommand({ @injectable() export class AIHistoryViewContribution extends AIViewContribution implements TabBarToolbarContribution { - recordingService: CommunicationRecordingService; - constructor(@inject(CommunicationRecordingService) recordingService: CommunicationRecordingService) { + @inject(CommunicationRecordingService) private recordingService: CommunicationRecordingService + + constructor() { super({ widgetId: AIHistoryView.ID, widgetName: AIHistoryView.LABEL, @@ -58,7 +59,6 @@ export class AIHistoryViewContribution extends AIViewContribution }, toggleCommandId: AI_HISTORY_TOGGLE_COMMAND_ID, }); - this.recordingService = recordingService; } async initializeLayout(_app: FrontendApplication): Promise { @@ -71,25 +71,25 @@ export class AIHistoryViewContribution extends AIViewContribution execute: () => this.openView({ activate: true }), }); registry.registerCommand(AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY, { - isEnabled: widget => this.withWidget(widget, () => !widget.isChronologial), - isVisible: widget => this.withWidget(widget, () => !widget.isChronologial), - execute: widget => this.withWidget(widget, chatWidget => { - widget.sortHistory(true); + isEnabled: widget => this.withHistoryWidget(widget, historyView => !historyView.isChronologial), + isVisible: widget => this.withHistoryWidget(widget, historyView => !historyView.isChronologial), + execute: widget => this.withHistoryWidget(widget, historyView => { + historyView.sortHistory(true); return true; }) }); registry.registerCommand(AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY, { - isEnabled: widget => this.withWidget(widget, () => widget.isChronologial), - isVisible: widget => this.withWidget(widget, () => widget.isChronologial), - execute: widget => this.withWidget(widget, chatWidget => { - widget.sortHistory(false); + isEnabled: widget => this.withHistoryWidget(widget, historyView => historyView.isChronologial), + isVisible: widget => this.withHistoryWidget(widget, historyView => historyView.isChronologial), + execute: widget => this.withHistoryWidget(widget, historyView => { + historyView.sortHistory(false); return true; }) }); registry.registerCommand(AI_HISTORY_VIEW_CLEAR, { - isEnabled: widget => this.withWidget(widget, () => true), - isVisible: widget => this.withWidget(widget, () => true), - execute: widget => this.withWidget(widget, chatWidget => { + isEnabled: widget => this.withHistoryWidget(widget), + isVisible: widget => this.withHistoryWidget(widget), + execute: widget => this.withHistoryWidget(widget, () => { this.clearHistory(); return true; }) @@ -99,8 +99,8 @@ export class AIHistoryViewContribution extends AIViewContribution this.recordingService.clearHistory(); } - protected withWidget( - widget: Widget | undefined = this.tryGetWidget(), + protected withHistoryWidget( + widget: unknown = this.tryGetWidget(), predicate: (output: AIHistoryView) => boolean = () => true ): boolean | false { return widget instanceof AIHistoryView ? predicate(widget) : false; @@ -122,24 +122,21 @@ export class AIHistoryViewContribution extends AIViewContribution id: AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY.id, command: AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY.id, tooltip: 'Sort chronologically', - isVisible: widget => this.isHistoryViewWidget(widget), + 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.isHistoryViewWidget(widget), + 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.isHistoryViewWidget(widget) + isVisible: widget => this.withHistoryWidget(widget) }); } - protected isHistoryViewWidget(widget?: Widget): boolean { - return !!widget && AIHistoryView.ID === widget.id; - } } diff --git a/packages/ai-history/src/browser/ai-history-widget.tsx b/packages/ai-history/src/browser/ai-history-widget.tsx index b984f00b750f6..95b1d6487c8b2 100644 --- a/packages/ai-history/src/browser/ai-history-widget.tsx +++ b/packages/ai-history/src/browser/ai-history-widget.tsx @@ -19,9 +19,9 @@ 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, Event } from '@theia/core'; +import { deepClone, Emitter } from '@theia/core'; -export namespace AIHistoryView { +namespace AIHistoryView { export interface State { chronological: boolean; } @@ -41,6 +41,7 @@ export class AIHistoryView extends ReactWidget implements StatefulWidget { protected _state: AIHistoryView.State = { chronological: false }; protected readonly onStateChangedEmitter = new Emitter(); + readonly onStateChanged = this.onStateChangedEmitter.event; constructor() { super(); @@ -60,10 +61,6 @@ export class AIHistoryView extends ReactWidget implements StatefulWidget { this.onStateChangedEmitter.fire(this._state); } - get onStateChanged(): Event { - return this.onStateChangedEmitter.event; - } - storeState(): object { return this.state; } @@ -139,6 +136,6 @@ export class AIHistoryView extends ReactWidget implements StatefulWidget { } get isChronologial(): boolean { - return !!this.state.chronological; + return this.state.chronological === true; } } From 77f2ba9487e70f49987ac06bb4fb51713e6e131a Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Tue, 1 Oct 2024 17:26:15 +0200 Subject: [PATCH 3/3] Fix linting error Signed-off-by: Jonas Helming --- packages/ai-history/src/browser/ai-history-contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ai-history/src/browser/ai-history-contribution.ts b/packages/ai-history/src/browser/ai-history-contribution.ts index dfdb808649e7f..0941e84dda3e8 100644 --- a/packages/ai-history/src/browser/ai-history-contribution.ts +++ b/packages/ai-history/src/browser/ai-history-contribution.ts @@ -47,7 +47,7 @@ export const AI_HISTORY_VIEW_CLEAR = Command.toLocalizedCommand({ @injectable() export class AIHistoryViewContribution extends AIViewContribution implements TabBarToolbarContribution { - @inject(CommunicationRecordingService) private recordingService: CommunicationRecordingService + @inject(CommunicationRecordingService) private recordingService: CommunicationRecordingService; constructor() { super({