Skip to content

Commit ecba028

Browse files
committed
Allow to order and clear AI History view
fixed #14183 Signed-off-by: Jonas Helming <[email protected]>
1 parent b80aa74 commit ecba028

File tree

5 files changed

+166
-11
lines changed

5 files changed

+166
-11
lines changed

packages/ai-core/src/common/communication-recording-service.ts

+3
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,7 @@ export interface CommunicationRecordingService {
4141
readonly onDidRecordResponse: Event<CommunicationResponseEntry>;
4242

4343
getHistory(agentId: string): CommunicationHistory;
44+
45+
clearHistory(): void;
46+
readonly onStructuralChange: Event<void>;
4447
}

packages/ai-history/src/browser/ai-history-contribution.ts

+101-8
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,42 @@
1313
//
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
16-
import { FrontendApplication } from '@theia/core/lib/browser';
16+
import { FrontendApplication, Widget, codicon } from '@theia/core/lib/browser';
1717
import { AIViewContribution } from '@theia/ai-core/lib/browser';
18-
import { injectable } from '@theia/core/shared/inversify';
18+
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
1919
import { AIHistoryView } from './ai-history-widget';
20-
import { Command, CommandRegistry } from '@theia/core';
20+
import { Command, CommandRegistry, Emitter } from '@theia/core';
21+
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
22+
import { CommunicationRecordingService } from '@theia/ai-core';
2123

2224
export const AI_HISTORY_TOGGLE_COMMAND_ID = 'aiHistory:toggle';
2325
export const OPEN_AI_HISTORY_VIEW = Command.toLocalizedCommand({
2426
id: 'aiHistory:open',
2527
label: 'Open AI History view',
2628
});
2729

30+
export const AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY = Command.toLocalizedCommand({
31+
id: 'aiHistory:sortChronologically',
32+
label: 'AI History: Sort chronologically',
33+
iconClass: codicon('arrow-down')
34+
});
35+
36+
export const AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY = Command.toLocalizedCommand({
37+
id: 'aiHistory:sortReverseChronologically',
38+
label: 'AI History: Sort reverse chronologically',
39+
iconClass: codicon('arrow-up')
40+
});
41+
42+
export const AI_HISTORY_VIEW_CLEAR = Command.toLocalizedCommand({
43+
id: 'aiHistory:clear',
44+
label: 'AI History: Clear History',
45+
iconClass: codicon('clear-all')
46+
});
47+
2848
@injectable()
29-
export class AIHistoryViewContribution extends AIViewContribution<AIHistoryView> {
30-
constructor() {
49+
export class AIHistoryViewContribution extends AIViewContribution<AIHistoryView> implements TabBarToolbarContribution {
50+
recordingService: CommunicationRecordingService;
51+
constructor(@inject(CommunicationRecordingService) recordingService: CommunicationRecordingService) {
3152
super({
3253
widgetId: AIHistoryView.ID,
3354
widgetName: AIHistoryView.LABEL,
@@ -37,16 +58,88 @@ export class AIHistoryViewContribution extends AIViewContribution<AIHistoryView>
3758
},
3859
toggleCommandId: AI_HISTORY_TOGGLE_COMMAND_ID,
3960
});
61+
this.recordingService = recordingService;
4062
}
4163

4264
async initializeLayout(_app: FrontendApplication): Promise<void> {
4365
await this.openView();
4466
}
4567

46-
override registerCommands(commands: CommandRegistry): void {
47-
super.registerCommands(commands);
48-
commands.registerCommand(OPEN_AI_HISTORY_VIEW, {
68+
override registerCommands(registry: CommandRegistry): void {
69+
super.registerCommands(registry);
70+
registry.registerCommand(OPEN_AI_HISTORY_VIEW, {
4971
execute: () => this.openView({ activate: true }),
5072
});
73+
registry.registerCommand(AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY, {
74+
isEnabled: widget => this.withWidget(widget, () => !widget.isChronologial),
75+
isVisible: widget => this.withWidget(widget, () => !widget.isChronologial),
76+
execute: widget => this.withWidget(widget, chatWidget => {
77+
widget.sortHistory(true);
78+
return true;
79+
})
80+
});
81+
registry.registerCommand(AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY, {
82+
isEnabled: widget => this.withWidget(widget, () => widget.isChronologial),
83+
isVisible: widget => this.withWidget(widget, () => widget.isChronologial),
84+
execute: widget => this.withWidget(widget, chatWidget => {
85+
widget.sortHistory(false);
86+
return true;
87+
})
88+
});
89+
registry.registerCommand(AI_HISTORY_VIEW_CLEAR, {
90+
isEnabled: widget => this.withWidget(widget, () => true),
91+
isVisible: widget => this.withWidget(widget, () => true),
92+
execute: widget => this.withWidget(widget, chatWidget => {
93+
this.clearHistory();
94+
return true;
95+
})
96+
});
97+
}
98+
public clearHistory(): void {
99+
this.recordingService.clearHistory();
100+
}
101+
102+
protected withWidget(
103+
widget: Widget | undefined = this.tryGetWidget(),
104+
predicate: (output: AIHistoryView) => boolean = () => true
105+
): boolean | false {
106+
return widget instanceof AIHistoryView ? predicate(widget) : false;
107+
}
108+
109+
protected readonly onAIHistoryWidgetStateChangedEmitter = new Emitter<void>();
110+
protected readonly onAIHistoryWidgettStateChanged = this.onAIHistoryWidgetStateChangedEmitter.event;
111+
112+
@postConstruct()
113+
protected override init(): void {
114+
super.init();
115+
this.widget.then(widget => {
116+
widget.onStateChanged(() => this.onAIHistoryWidgetStateChangedEmitter.fire());
117+
});
118+
}
119+
120+
registerToolbarItems(registry: TabBarToolbarRegistry): void {
121+
registry.registerItem({
122+
id: AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY.id,
123+
command: AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY.id,
124+
tooltip: 'Sort chronologically',
125+
isVisible: widget => this.isHistoryViewWidget(widget),
126+
onDidChange: this.onAIHistoryWidgettStateChanged
127+
});
128+
registry.registerItem({
129+
id: AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY.id,
130+
command: AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY.id,
131+
tooltip: 'Sort reverse chronologically',
132+
isVisible: widget => this.isHistoryViewWidget(widget),
133+
onDidChange: this.onAIHistoryWidgettStateChanged
134+
});
135+
registry.registerItem({
136+
id: AI_HISTORY_VIEW_CLEAR.id,
137+
command: AI_HISTORY_VIEW_CLEAR.id,
138+
tooltip: 'Clear History of all agents',
139+
isVisible: widget => this.isHistoryViewWidget(widget)
140+
});
141+
}
142+
protected isHistoryViewWidget(widget?: Widget): boolean {
143+
return !!widget && AIHistoryView.ID === widget.id;
51144
}
52145
}

packages/ai-history/src/browser/ai-history-frontend-module.ts

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ILogger } from '@theia/core';
2121
import { AIHistoryViewContribution } from './ai-history-contribution';
2222
import { AIHistoryView } from './ai-history-widget';
2323
import '../../src/browser/style/ai-history.css';
24+
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
2425

2526
export default new ContainerModule(bind => {
2627
bind(DefaultCommunicationRecordingService).toSelf().inSingletonScope();
@@ -38,4 +39,6 @@ export default new ContainerModule(bind => {
3839
id: AIHistoryView.ID,
3940
createWidget: () => context.container.get<AIHistoryView>(AIHistoryView)
4041
})).inSingletonScope();
42+
bind(TabBarToolbarContribution).toService(AIHistoryViewContribution);
43+
4144
});

packages/ai-history/src/browser/ai-history-widget.tsx

+51-3
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616
import { Agent, AgentService, CommunicationRecordingService, CommunicationRequestEntry, CommunicationResponseEntry } from '@theia/ai-core';
17-
import { codicon, ReactWidget } from '@theia/core/lib/browser';
17+
import { codicon, ReactWidget, StatefulWidget } from '@theia/core/lib/browser';
1818
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
1919
import * as React from '@theia/core/shared/react';
2020
import { CommunicationCard } from './ai-history-communication-card';
2121
import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component';
22+
import { deepClone, Emitter, Event } from '@theia/core';
23+
24+
export namespace AIHistoryView {
25+
export interface State {
26+
chronological: boolean;
27+
}
28+
}
2229

2330
@injectable()
24-
export class AIHistoryView extends ReactWidget {
31+
export class AIHistoryView extends ReactWidget implements StatefulWidget {
2532
@inject(CommunicationRecordingService)
2633
protected recordingService: CommunicationRecordingService;
2734
@inject(AgentService)
@@ -32,6 +39,9 @@ export class AIHistoryView extends ReactWidget {
3239

3340
protected selectedAgent?: Agent;
3441

42+
protected _state: AIHistoryView.State = { chronological: false };
43+
protected readonly onStateChangedEmitter = new Emitter<AIHistoryView.State>();
44+
3545
constructor() {
3646
super();
3747
this.id = AIHistoryView.ID;
@@ -41,11 +51,38 @@ export class AIHistoryView extends ReactWidget {
4151
this.title.iconClass = codicon('history');
4252
}
4353

54+
protected get state(): AIHistoryView.State {
55+
return this._state;
56+
}
57+
58+
protected set state(state: AIHistoryView.State) {
59+
this._state = state;
60+
this.onStateChangedEmitter.fire(this._state);
61+
}
62+
63+
get onStateChanged(): Event<AIHistoryView.State> {
64+
return this.onStateChangedEmitter.event;
65+
}
66+
67+
storeState(): object {
68+
return this.state;
69+
}
70+
71+
restoreState(oldState: object & Partial<AIHistoryView.State>): void {
72+
const copy = deepClone(this.state);
73+
if (oldState.chronological) {
74+
copy.chronological = oldState.chronological;
75+
}
76+
this.state = copy;
77+
}
78+
4479
@postConstruct()
4580
protected init(): void {
4681
this.update();
4782
this.toDispose.push(this.recordingService.onDidRecordRequest(entry => this.historyContentUpdated(entry)));
4883
this.toDispose.push(this.recordingService.onDidRecordResponse(entry => this.historyContentUpdated(entry)));
84+
this.toDispose.push(this.recordingService.onStructuralChange(() => this.update()));
85+
this.toDispose.push(this.onStateChanged(newState => this.update()));
4986
this.selectAgent(this.agentService.getAllAgents()[0]);
5087
}
5188

@@ -82,15 +119,26 @@ export class AIHistoryView extends ReactWidget {
82119
if (!this.selectedAgent) {
83120
return <div className='theia-card no-content'>No agent selected.</div>;
84121
}
85-
const history = this.recordingService.getHistory(this.selectedAgent.id);
122+
const history = [...this.recordingService.getHistory(this.selectedAgent.id)];
86123
if (history.length === 0) {
87124
return <div className='theia-card no-content'>No history available for the selected agent '{this.selectedAgent.name}'.</div>;
88125
}
126+
if (!this.state.chronological) {
127+
history.reverse();
128+
}
89129
return history.map(entry => <CommunicationCard key={entry.requestId} entry={entry} />);
90130
}
91131

92132
protected onClick(e: React.MouseEvent<HTMLDivElement>, agent: Agent): void {
93133
e.stopPropagation();
94134
this.selectAgent(agent);
95135
}
136+
137+
public sortHistory(chronological: boolean): void {
138+
this.state = { ...deepClone(this.state), chronological: chronological };
139+
}
140+
141+
get isChronologial(): boolean {
142+
return !!this.state.chronological;
143+
}
96144
}

packages/ai-history/src/common/communication-recording-service.ts

+8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export class DefaultCommunicationRecordingService implements CommunicationRecord
2929
protected onDidRecordResponseEmitter = new Emitter<CommunicationResponseEntry>();
3030
readonly onDidRecordResponse: Event<CommunicationResponseEntry> = this.onDidRecordResponseEmitter.event;
3131

32+
protected onStructuralChangeEmitter = new Emitter<void>();
33+
readonly onStructuralChange: Event<void> = this.onStructuralChangeEmitter.event;
34+
3235
protected history: Map<string, CommunicationHistory> = new Map();
3336

3437
getHistory(agentId: string): CommunicationHistory {
@@ -60,4 +63,9 @@ export class DefaultCommunicationRecordingService implements CommunicationRecord
6063
}
6164
}
6265
}
66+
67+
clearHistory(): void {
68+
this.history.clear();
69+
this.onStructuralChangeEmitter.fire(undefined);
70+
}
6371
}

0 commit comments

Comments
 (0)