Skip to content

Commit

Permalink
Allow to order and clear AI History view (#14233)
Browse files Browse the repository at this point in the history
* Allow to order and clear AI History view

fixed #14183

Signed-off-by: Jonas Helming <[email protected]>
  • Loading branch information
JonasHelming authored Oct 9, 2024
1 parent 6b7ceb7 commit 43c4fe7
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ export interface CommunicationRecordingService {
readonly onDidRecordResponse: Event<CommunicationResponseEntry>;

getHistory(agentId: string): CommunicationHistory;

clearHistory(): void;
readonly onStructuralChange: Event<void>;
}
104 changes: 97 additions & 7 deletions packages/ai-history/src/browser/ai-history-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,42 @@
//
// 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({
id: 'aiHistory:open',
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<AIHistoryView> {
export class AIHistoryViewContribution extends AIViewContribution<AIHistoryView> implements TabBarToolbarContribution {
@inject(CommunicationRecordingService) private recordingService: CommunicationRecordingService;

constructor() {
super({
widgetId: AIHistoryView.ID,
Expand All @@ -43,10 +65,78 @@ export class AIHistoryViewContribution extends AIViewContribution<AIHistoryView>
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<void>();
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)
});
}
}
3 changes: 3 additions & 0 deletions packages/ai-history/src/browser/ai-history-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -38,4 +39,6 @@ export default new ContainerModule(bind => {
id: AIHistoryView.ID,
createWidget: () => context.container.get<AIHistoryView>(AIHistoryView)
})).inSingletonScope();
bind(TabBarToolbarContribution).toService(AIHistoryViewContribution);

});
51 changes: 48 additions & 3 deletions packages/ai-history/src/browser/ai-history-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -32,6 +39,10 @@ export class AIHistoryView extends ReactWidget {

protected selectedAgent?: Agent;

protected _state: AIHistoryView.State = { chronological: false };
protected readonly onStateChangedEmitter = new Emitter<AIHistoryView.State>();
readonly onStateChanged = this.onStateChangedEmitter.event;

constructor() {
super();
this.id = AIHistoryView.ID;
Expand All @@ -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<AIHistoryView.State>): 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]);
}

Expand Down Expand Up @@ -82,15 +116,26 @@ export class AIHistoryView extends ReactWidget {
if (!this.selectedAgent) {
return <div className='theia-card no-content'>No agent selected.</div>;
}
const history = this.recordingService.getHistory(this.selectedAgent.id);
const history = [...this.recordingService.getHistory(this.selectedAgent.id)];
if (history.length === 0) {
return <div className='theia-card no-content'>No history available for the selected agent '{this.selectedAgent.name}'.</div>;
}
if (!this.state.chronological) {
history.reverse();
}
return history.map(entry => <CommunicationCard key={entry.requestId} entry={entry} />);
}

protected onClick(e: React.MouseEvent<HTMLDivElement>, agent: Agent): void {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export class DefaultCommunicationRecordingService implements CommunicationRecord
protected onDidRecordResponseEmitter = new Emitter<CommunicationResponseEntry>();
readonly onDidRecordResponse: Event<CommunicationResponseEntry> = this.onDidRecordResponseEmitter.event;

protected onStructuralChangeEmitter = new Emitter<void>();
readonly onStructuralChange: Event<void> = this.onStructuralChangeEmitter.event;

protected history: Map<string, CommunicationHistory> = new Map();

getHistory(agentId: string): CommunicationHistory {
Expand Down Expand Up @@ -60,4 +63,9 @@ export class DefaultCommunicationRecordingService implements CommunicationRecord
}
}
}

clearHistory(): void {
this.history.clear();
this.onStructuralChangeEmitter.fire(undefined);
}
}

0 comments on commit 43c4fe7

Please sign in to comment.