Skip to content

Commit 1c27668

Browse files
committed
refactor: use communication model in AI history view
Refactors the AI history view to use the new AI communication model as data input. WIP Contributed on behalf of STMicroelectronics
1 parent 84546b7 commit 1c27668

File tree

5 files changed

+330
-32
lines changed

5 files changed

+330
-32
lines changed

packages/ai-core/src/common/language-model-service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export class LanguageModelServiceImpl implements LanguageModelService {
107107
semanticRequest = {
108108
id: languageModelRequest.requestId,
109109
requests: [],
110-
metadata: { agentId: languageModelRequest.agentId }
110+
metadata: { agent: languageModelRequest.agentId }
111111
};
112112
session.requests.push(semanticRequest);
113113
}
@@ -123,7 +123,7 @@ export class LanguageModelServiceImpl implements LanguageModelService {
123123

124124
semanticRequest.requests.push(aiRequest);
125125

126-
aiRequest.metadata.agentId = languageModelRequest.agentId;
126+
aiRequest.metadata.agent = languageModelRequest.agentId;
127127
aiRequest.metadata.timestamp = Date.now();
128128

129129
this.sessionChangedEmitter.fire({ type: 'requestAdded', id: languageModelRequest.subRequestId ?? languageModelRequest.requestId });

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { inject, injectable } from '@theia/core/shared/inversify';
1919
import { AIHistoryView } from './ai-history-widget';
2020
import { Command, CommandRegistry, Emitter, nls } from '@theia/core';
2121
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
22-
import { CommunicationRecordingService } from '@theia/ai-core';
22+
import { CommunicationRecordingService, LanguageModelService } from '@theia/ai-core';
2323

2424
export const AI_HISTORY_TOGGLE_COMMAND_ID = 'aiHistory:toggle';
2525
export const OPEN_AI_HISTORY_VIEW = Command.toLocalizedCommand({
@@ -48,6 +48,7 @@ export const AI_HISTORY_VIEW_CLEAR = Command.toLocalizedCommand({
4848
@injectable()
4949
export class AIHistoryViewContribution extends AIViewContribution<AIHistoryView> implements TabBarToolbarContribution {
5050
@inject(CommunicationRecordingService) private recordingService: CommunicationRecordingService;
51+
@inject(LanguageModelService) private languageModelService: LanguageModelService;
5152

5253
protected readonly chronologicalChangedEmitter = new Emitter<void>();
5354
protected readonly chronologicalStateChanged = this.chronologicalChangedEmitter.event;
@@ -102,6 +103,7 @@ export class AIHistoryViewContribution extends AIViewContribution<AIHistoryView>
102103
}
103104
public clearHistory(): void {
104105
this.recordingService.clearHistory();
106+
this.languageModelService.sessions = [];
105107
}
106108

107109
protected withHistoryWidget(
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2025 EclipseSource GmbH.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
import { AiRequest, AiSemanticRequest, LanguageModelMonitoredStreamResponse } from '@theia/ai-core/lib/common/language-model-interaction-model';
17+
import { nls } from '@theia/core';
18+
import * as React from '@theia/core/shared/react';
19+
20+
export interface SemanticRequestCardProps {
21+
semanticRequest: AiSemanticRequest;
22+
selectedAgentId?: string;
23+
}
24+
25+
export const SemanticRequestCard: React.FC<SemanticRequestCardProps> = ({ semanticRequest, selectedAgentId }) => {
26+
const formatTimestamp = (timestamp: number): string =>
27+
new Date(timestamp).toLocaleString(undefined, {
28+
year: 'numeric',
29+
month: 'short',
30+
day: 'numeric',
31+
hour: '2-digit',
32+
minute: '2-digit',
33+
second: '2-digit'
34+
});
35+
36+
const isHighlighted = selectedAgentId && (
37+
semanticRequest.metadata.agent === selectedAgentId ||
38+
semanticRequest.requests.some(req => req.metadata.agent === selectedAgentId)
39+
);
40+
41+
// Get the earliest timestamp from all sub-requests
42+
const earliestTimestamp = semanticRequest.requests.reduce((earliest, req) => {
43+
const timestamp = req.metadata.timestamp as number || 0;
44+
return timestamp && (!earliest || timestamp < earliest) ? timestamp : earliest;
45+
}, 0);
46+
47+
return (
48+
<div className={`theia-card semantic-request-card ${isHighlighted ? 'highlighted' : ''}`}
49+
role="article"
50+
aria-label={`Semantic request ${semanticRequest.id}`}>
51+
<div className='theia-card-meta'>
52+
<span className='theia-card-request-id'>
53+
{nls.localize('theia/ai/history/semantic-request-card/requestId', 'Request ID')}: {semanticRequest.id}
54+
</span>
55+
{semanticRequest.metadata.agent && (
56+
<span className='theia-card-agent-id'>
57+
{nls.localize('theia/ai/history/semantic-request-card/agentId', 'Agent')}: {semanticRequest.metadata.agent}
58+
</span>
59+
)}
60+
</div>
61+
<div className='theia-card-content'>
62+
<h2>{nls.localize('theia/ai/history/semantic-request-card/semanticRequest', 'Semantic Request')}</h2>
63+
64+
<div className='sub-requests-container'>
65+
{semanticRequest.requests.map((request, index) => (
66+
<SubRequestCard
67+
key={request.id}
68+
request={request}
69+
index={index}
70+
isHighlighted={selectedAgentId === request.metadata.agent}
71+
/>
72+
))}
73+
</div>
74+
</div>
75+
<div className='theia-card-meta'>
76+
{earliestTimestamp > 0 && (
77+
<span className='theia-card-timestamp'>
78+
{nls.localize('theia/ai/history/semantic-request-card/timestamp', 'Started')}: {formatTimestamp(earliestTimestamp)}
79+
</span>
80+
)}
81+
</div>
82+
</div>
83+
);
84+
};
85+
86+
interface SubRequestCardProps {
87+
request: AiRequest;
88+
index: number;
89+
isHighlighted: boolean;
90+
}
91+
92+
const SubRequestCard: React.FC<SubRequestCardProps> = ({ request, index, isHighlighted }) => {
93+
const formatJson = (data: unknown): string => {
94+
try {
95+
return JSON.stringify(data, undefined, 2);
96+
} catch (error) {
97+
console.error('Error formatting JSON:', error);
98+
return 'Error formatting data';
99+
}
100+
};
101+
102+
const formatTimestamp = (timestamp: number | undefined): string =>
103+
timestamp ? new Date(timestamp).toLocaleString(undefined, {
104+
year: 'numeric',
105+
month: 'short',
106+
day: 'numeric',
107+
hour: '2-digit',
108+
minute: '2-digit',
109+
second: '2-digit'
110+
}) : 'N/A';
111+
112+
const isStreamResponse = 'parts' in request.response;
113+
114+
const getResponseContent = () => {
115+
if (isStreamResponse) {
116+
const streamResponse = request.response as LanguageModelMonitoredStreamResponse;
117+
return streamResponse.parts.map((part, i) => (
118+
<div key={`part-${i}`} className="stream-part">
119+
<pre>{JSON.stringify(part, undefined, 2)}</pre>
120+
</div>
121+
));
122+
} else {
123+
return <pre>{formatJson(request.response)}</pre>;
124+
}
125+
};
126+
127+
return (
128+
<div className={`sub-request-card ${isHighlighted ? 'highlighted' : ''}`}>
129+
<div className='sub-request-header'>
130+
<h3>{nls.localize('theia/ai/history/sub-request-card/title', 'Sub-Request')} {index + 1}</h3>
131+
<span className='sub-request-id'>ID: {request.id}</span>
132+
{request.metadata.agent && (
133+
<span className='sub-request-agent'>
134+
{nls.localize('theia/ai/history/sub-request-card/agent', 'Agent')}: {request.metadata.agent}
135+
</span>
136+
)}
137+
<span className='sub-request-model'>
138+
{nls.localize('theia/ai/history/sub-request-card/model', 'Model')}: {request.languageModel}
139+
</span>
140+
</div>
141+
142+
<div className='sub-request-content'>
143+
<details>
144+
<summary>
145+
{nls.localize('theia/ai/history/sub-request-card/request', 'Request')}
146+
</summary>
147+
<div className='request-content'>
148+
<pre>{formatJson(request.request)}</pre>
149+
</div>
150+
</details>
151+
152+
<details>
153+
<summary>
154+
{nls.localize('theia/ai/history/sub-request-card/response', 'Response')}
155+
</summary>
156+
<div className='response-content'>
157+
{getResponseContent()}
158+
</div>
159+
</details>
160+
</div>
161+
162+
<div className='sub-request-meta'>
163+
{request.metadata.timestamp && (
164+
<span className='sub-request-timestamp'>
165+
{nls.localize('theia/ai/history/sub-request-card/timestamp', 'Timestamp')}: {formatTimestamp(request.metadata.timestamp as number)}
166+
</span>
167+
)}
168+
</div>
169+
</div>
170+
);
171+
};

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

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,32 @@
1313
//
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
16-
import { Agent, AgentService, CommunicationRecordingService, CommunicationRequestEntry, CommunicationResponseEntry } from '@theia/ai-core';
16+
import { Agent, AgentService, LanguageModelService, SessionEvent } from '@theia/ai-core';
17+
import { AiSemanticRequest } from '@theia/ai-core/lib/common/language-model-interaction-model';
1718
import { codicon, ReactWidget, StatefulWidget } from '@theia/core/lib/browser';
1819
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
1920
import * as React from '@theia/core/shared/react';
20-
import { CommunicationCard } from './ai-history-communication-card';
21+
import { SemanticRequestCard } from './ai-history-semantic-request-card';
2122
import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component';
2223
import { deepClone, nls } from '@theia/core';
2324

2425
namespace AIHistoryView {
2526
export interface State {
2627
chronological: boolean;
28+
selectedAgentId?: string;
2729
}
2830
}
2931

3032
@injectable()
3133
export class AIHistoryView extends ReactWidget implements StatefulWidget {
32-
@inject(CommunicationRecordingService)
33-
protected recordingService: CommunicationRecordingService;
34+
@inject(LanguageModelService)
35+
protected languageModelService: LanguageModelService;
3436
@inject(AgentService)
3537
protected readonly agentService: AgentService;
3638

3739
public static ID = 'ai-history-widget';
3840
static LABEL = nls.localize('theia/ai/history/view/label', 'AI Agent History [Alpha]');
3941

40-
protected selectedAgent?: Agent;
41-
4242
protected _state: AIHistoryView.State = { chronological: false };
4343

4444
constructor() {
@@ -74,27 +74,21 @@ export class AIHistoryView extends ReactWidget implements StatefulWidget {
7474
@postConstruct()
7575
protected init(): void {
7676
this.update();
77-
this.toDispose.push(this.recordingService.onDidRecordRequest(entry => this.historyContentUpdated(entry)));
78-
this.toDispose.push(this.recordingService.onDidRecordResponse(entry => this.historyContentUpdated(entry)));
79-
this.toDispose.push(this.recordingService.onStructuralChange(() => this.update()));
77+
this.toDispose.push(this.languageModelService.onSessionChanged((event: SessionEvent) => this.historyContentUpdated(event)));
8078
this.selectAgent(this.agentService.getAllAgents()[0]);
8179
}
8280

8381
protected selectAgent(agent: Agent | undefined): void {
84-
this.selectedAgent = agent;
85-
this.update();
82+
this.state = { ...this.state, selectedAgentId: agent?.id };
8683
}
8784

88-
protected historyContentUpdated(entry: CommunicationRequestEntry | CommunicationResponseEntry): void {
89-
if (entry.agentId === this.selectedAgent?.id) {
90-
this.update();
91-
}
85+
protected historyContentUpdated(event: SessionEvent): void {
86+
this.update();
9287
}
9388

9489
render(): React.ReactNode {
9590
const selectionChange = (value: SelectOption) => {
96-
this.selectedAgent = this.agentService.getAllAgents().find(agent => agent.id === value.value);
97-
this.update();
91+
this.selectAgent(this.agentService.getAllAgents().find(agent => agent.id === value.value));
9892
};
9993
const agents = this.agentService.getAllAgents();
10094
if (agents.length === 0) {
@@ -112,7 +106,7 @@ export class AIHistoryView extends ReactWidget implements StatefulWidget {
112106
description: agent.description || ''
113107
}))}
114108
onChange={selectionChange}
115-
defaultValue={this.selectedAgent?.id} />
109+
defaultValue={this.state.selectedAgentId} />
116110
<div className='agent-history'>
117111
{this.renderHistory()}
118112
</div>
@@ -121,24 +115,41 @@ export class AIHistoryView extends ReactWidget implements StatefulWidget {
121115
}
122116

123117
protected renderHistory(): React.ReactNode {
124-
if (!this.selectedAgent) {
118+
if (!this.state.selectedAgentId) {
125119
return <div className='theia-card no-content'>{nls.localize('theia/ai/history/view/noAgentSelected', 'No agent selected.')}</div>;
126120
}
127-
const history = [...this.recordingService.getHistory(this.selectedAgent.id)];
128-
if (history.length === 0) {
121+
122+
const semanticRequests = this.getSemanticRequestsByAgent(this.state.selectedAgentId);
123+
124+
if (semanticRequests.length === 0) {
125+
const selectedAgent = this.agentService.getAllAgents().find(agent => agent.id === this.state.selectedAgentId);
129126
return <div className='theia-card no-content'>
130-
{nls.localize('theia/ai/history/view/noHistoryForAgent', 'No history available for the selected agent \'{0}\'', this.selectedAgent.name)}
127+
{nls.localize('theia/ai/history/view/noHistoryForAgent', 'No history available for the selected agent \'{0}\'', selectedAgent?.name || this.state.selectedAgentId)}
131128
</div>;
132129
}
133-
if (!this.state.chronological) {
134-
history.reverse();
135-
}
136-
return history.map(entry => <CommunicationCard key={entry.requestId} entry={entry} />);
130+
131+
// Sort requests by timestamp (using the first sub-request's timestamp)
132+
const sortedRequests = [...semanticRequests].sort((a, b) => {
133+
const aTimestamp = a.requests[0]?.metadata.timestamp as number || 0;
134+
const bTimestamp = b.requests[0]?.metadata.timestamp as number || 0;
135+
return this.state.chronological ? aTimestamp - bTimestamp : bTimestamp - aTimestamp;
136+
});
137+
138+
return sortedRequests.map(request => <SemanticRequestCard key={request.id} semanticRequest={request} selectedAgentId={this.state.selectedAgentId} />);
137139
}
138140

139-
protected onClick(e: React.MouseEvent<HTMLDivElement>, agent: Agent): void {
140-
e.stopPropagation();
141-
this.selectAgent(agent);
141+
/**
142+
* Get all semantic requests for a specific agent.
143+
* Includes all requests in which the agent is involved, either as the main request or as a sub-request.
144+
* @param agentId The agent ID to filter by
145+
*/
146+
protected getSemanticRequestsByAgent(agentId: string): AiSemanticRequest[] {
147+
return this.languageModelService.sessions.flatMap(session =>
148+
session.requests.filter(request =>
149+
request.metadata.agent === agentId ||
150+
request.requests.some(subRequest => subRequest.metadata.agent === agentId)
151+
)
152+
);
142153
}
143154

144155
public sortHistory(chronological: boolean): void {

0 commit comments

Comments
 (0)