Skip to content

Commit

Permalink
Allow to order and clear AI History view
Browse files Browse the repository at this point in the history
fixed #14183

Signed-off-by: Jonas Helming <[email protected]>
  • Loading branch information
JonasHelming committed Sep 29, 2024
1 parent b80aa74 commit 3079381
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 11 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>;
}
108 changes: 100 additions & 8 deletions packages/ai-history/src/browser/ai-history-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,43 @@
//
// 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 } 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 { codicon } from '@theia/core/lib/browser';
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> {
constructor() {
export class AIHistoryViewContribution extends AIViewContribution<AIHistoryView> implements TabBarToolbarContribution {
recordingService: CommunicationRecordingService;
constructor(@inject(CommunicationRecordingService) recordingService: CommunicationRecordingService) {
super({
widgetId: AIHistoryView.ID,
widgetName: AIHistoryView.LABEL,
Expand All @@ -37,16 +59,86 @@ export class AIHistoryViewContribution extends AIViewContribution<AIHistoryView>
},
toggleCommandId: AI_HISTORY_TOGGLE_COMMAND_ID,
});
this.recordingService = recordingService;
}

async initializeLayout(_app: FrontendApplication): Promise<void> {
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;
})
});
}
clearHistory() {
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<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.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;
}
}
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);

});
58 changes: 55 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,22 @@
// 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)
Expand All @@ -32,6 +40,11 @@ export class AIHistoryView extends ReactWidget {

protected selectedAgent?: Agent;


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


constructor() {
super();
this.id = AIHistoryView.ID;
Expand All @@ -41,11 +54,39 @@ 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<AIHistoryView.State> {
return this.onStateChangedEmitter.event;
}

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 +123,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) {
this.state = { ...deepClone(this.state), chronological: chronological };
}

get isChronologial(): boolean {
return !!this.state.chronological;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { inject, injectable, named } from '@theia/core/shared/inversify';
@injectable()
export class DefaultCommunicationRecordingService implements CommunicationRecordingService {


@inject(ILogger) @named('llm-communication-recorder')
protected logger: ILogger;

Expand All @@ -29,6 +30,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 +64,9 @@ export class DefaultCommunicationRecordingService implements CommunicationRecord
}
}
}

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

0 comments on commit 3079381

Please sign in to comment.