deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,32 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - AI HISTORY EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/ai-history` extension offers a framework for agents to record their requests and responses.
It also offers a view to inspect the history.
## Additional Information
- [API documentation for `@theia/ai-history`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_ai-history.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,54 @@
{
"name": "@theia/ai-history",
"version": "1.68.0",
"description": "Theia - AI communication history",
"dependencies": {
"@theia/ai-core": "1.68.0",
"@theia/ai-chat-ui": "1.68.0",
"@theia/core": "1.68.0",
"@theia/filesystem": "1.68.0",
"@theia/output": "1.68.0",
"@theia/workspace": "1.68.0",
"tslib": "^2.6.2"
},
"main": "lib/common",
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"frontend": "lib/browser/ai-history-frontend-module"
}
],
"keywords": [
"theia-extension"
],
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"lib",
"src"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,240 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { FrontendApplication, codicon } from '@theia/core/lib/browser';
import { AIViewContribution } from '@theia/ai-core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { AIHistoryView } from './ai-history-widget';
import { Command, CommandRegistry, Emitter, nls } from '@theia/core';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { LanguageModelService } from '@theia/ai-core';
import { ChatViewWidget } from '@theia/ai-chat-ui/lib/browser/chat-view-widget';
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_TOGGLE_COMPACT = Command.toLocalizedCommand({
id: 'aiHistory:toggleCompact',
label: 'AI History: Toggle compact view',
iconClass: codicon('list-flat')
});
export const AI_HISTORY_VIEW_TOGGLE_RAW = Command.toLocalizedCommand({
id: 'aiHistory:toggleRaw',
label: 'AI History: Toggle raw view',
iconClass: codicon('list-tree')
});
export const AI_HISTORY_VIEW_TOGGLE_RENDER_NEWLINES = Command.toLocalizedCommand({
id: 'aiHistory:toggleRenderNewlines',
label: 'AI History: Interpret newlines',
iconClass: codicon('newline')
});
export const AI_HISTORY_VIEW_TOGGLE_HIDE_NEWLINES = Command.toLocalizedCommand({
id: 'aiHistory:toggleHideNewlines',
label: 'AI History: Stop interpreting newlines',
iconClass: codicon('no-newline')
});
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> implements TabBarToolbarContribution {
@inject(LanguageModelService) private languageModelService: LanguageModelService;
protected readonly chronologicalChangedEmitter = new Emitter<void>();
protected readonly chronologicalStateChanged = this.chronologicalChangedEmitter.event;
protected readonly compactViewChangedEmitter = new Emitter<void>();
protected readonly compactViewStateChanged = this.compactViewChangedEmitter.event;
protected readonly renderNewlinesChangedEmitter = new Emitter<void>();
protected readonly renderNewlinesStateChanged = this.renderNewlinesChangedEmitter.event;
constructor() {
super({
widgetId: AIHistoryView.ID,
widgetName: AIHistoryView.LABEL,
defaultWidgetOptions: {
area: 'bottom',
rank: 100
},
toggleCommandId: AI_HISTORY_TOGGLE_COMMAND_ID,
});
}
async initializeLayout(_app: FrontendApplication): Promise<void> {
await this.openView();
}
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);
this.chronologicalChangedEmitter.fire();
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);
this.chronologicalChangedEmitter.fire();
return true;
})
});
registry.registerCommand(AI_HISTORY_VIEW_TOGGLE_COMPACT, {
isEnabled: widget => this.withHistoryWidget(widget),
isVisible: widget => this.withHistoryWidget(widget, historyView => !historyView.isCompactView),
execute: widget => this.withHistoryWidget(widget, historyView => {
historyView.toggleCompactView();
this.compactViewChangedEmitter.fire();
return true;
})
});
registry.registerCommand(AI_HISTORY_VIEW_TOGGLE_RAW, {
isEnabled: widget => this.withHistoryWidget(widget),
isVisible: widget => this.withHistoryWidget(widget, historyView => historyView.isCompactView),
execute: widget => this.withHistoryWidget(widget, historyView => {
historyView.toggleCompactView();
this.compactViewChangedEmitter.fire();
return true;
})
});
registry.registerCommand(AI_HISTORY_VIEW_TOGGLE_RENDER_NEWLINES, {
isEnabled: widget => this.withHistoryWidget(widget),
isVisible: widget => this.withHistoryWidget(widget, historyView => !historyView.isRenderNewlines),
execute: widget => this.withHistoryWidget(widget, historyView => {
historyView.toggleRenderNewlines();
this.renderNewlinesChangedEmitter.fire();
return true;
})
});
registry.registerCommand(AI_HISTORY_VIEW_TOGGLE_HIDE_NEWLINES, {
isEnabled: widget => this.withHistoryWidget(widget),
isVisible: widget => this.withHistoryWidget(widget, historyView => historyView.isRenderNewlines),
execute: widget => this.withHistoryWidget(widget, historyView => {
historyView.toggleRenderNewlines();
this.renderNewlinesChangedEmitter.fire();
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.languageModelService.sessions = [];
}
protected withHistoryWidget(
widget: unknown = this.tryGetWidget(),
predicate: (output: AIHistoryView) => boolean = () => true
): boolean | false {
return widget instanceof AIHistoryView ? predicate(widget) : false;
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY.id,
command: AI_HISTORY_VIEW_SORT_CHRONOLOGICALLY.id,
tooltip: nls.localize('theia/ai/history/sortChronologically/tooltip', 'Sort chronologically'),
isVisible: widget => this.withHistoryWidget(widget),
onDidChange: this.chronologicalStateChanged
});
registry.registerItem({
id: AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY.id,
command: AI_HISTORY_VIEW_SORT_REVERSE_CHRONOLOGICALLY.id,
tooltip: nls.localize('theia/ai/history/sortReverseChronologically/tooltip', 'Sort reverse chronologically'),
isVisible: widget => this.withHistoryWidget(widget),
onDidChange: this.chronologicalStateChanged
});
registry.registerItem({
id: AI_HISTORY_VIEW_TOGGLE_COMPACT.id,
command: AI_HISTORY_VIEW_TOGGLE_COMPACT.id,
tooltip: nls.localize('theia/ai/history/toggleCompact/tooltip', 'Show compact view'),
isVisible: widget => this.withHistoryWidget(widget, historyView => !historyView.isCompactView),
onDidChange: this.compactViewStateChanged
});
registry.registerItem({
id: AI_HISTORY_VIEW_TOGGLE_RAW.id,
command: AI_HISTORY_VIEW_TOGGLE_RAW.id,
tooltip: nls.localize('theia/ai/history/toggleRaw/tooltip', 'Show raw view'),
isVisible: widget => this.withHistoryWidget(widget, historyView => historyView.isCompactView),
onDidChange: this.compactViewStateChanged
});
registry.registerItem({
id: AI_HISTORY_VIEW_TOGGLE_RENDER_NEWLINES.id,
command: AI_HISTORY_VIEW_TOGGLE_RENDER_NEWLINES.id,
tooltip: nls.localize('theia/ai/history/toggleRenderNewlines/tooltip', 'Interpret newlines'),
isVisible: widget => this.withHistoryWidget(widget, historyView => !historyView.isRenderNewlines),
onDidChange: this.renderNewlinesStateChanged
});
registry.registerItem({
id: AI_HISTORY_VIEW_TOGGLE_HIDE_NEWLINES.id,
command: AI_HISTORY_VIEW_TOGGLE_HIDE_NEWLINES.id,
tooltip: nls.localize('theia/ai/history/toggleHideNewlines/tooltip', 'Stop interpreting newlines'),
isVisible: widget => this.withHistoryWidget(widget, historyView => historyView.isRenderNewlines),
onDidChange: this.renderNewlinesStateChanged
});
registry.registerItem({
id: AI_HISTORY_VIEW_CLEAR.id,
command: AI_HISTORY_VIEW_CLEAR.id,
tooltip: nls.localize('theia/ai/history/clear/tooltip', 'Clear History of all agents'),
isVisible: widget => this.withHistoryWidget(widget)
});
// Register the AI History view command for the chat view
registry.registerItem({
id: 'chat-view.' + OPEN_AI_HISTORY_VIEW.id,
command: OPEN_AI_HISTORY_VIEW.id,
tooltip: nls.localize('theia/ai/history/open-history-tooltip', 'Open AI history...'),
group: 'ai-settings',
priority: 1,
isVisible: widget => this.activationService.isActive && widget instanceof ChatViewWidget
});
}
}

View File

@@ -0,0 +1,253 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import {
LanguageModelExchangeRequest,
LanguageModelExchange,
LanguageModelMonitoredStreamResponse,
LanguageModelExchangeRequestResponse
} from '@theia/ai-core/lib/common/language-model-interaction-model';
import { nls } from '@theia/core';
import * as React from '@theia/core/shared/react';
const getTextFromResponse = (response: LanguageModelExchangeRequestResponse): string => {
// Handle monitored stream response
if ('parts' in response) {
let result = '';
for (const chunk of response.parts) {
if ('content' in chunk && chunk.content) {
result += chunk.content;
}
}
return result;
}
// Handle text response
if ('text' in response) {
return response.text;
}
// Handle parsed response
if ('content' in response) {
return response.content;
}
return JSON.stringify(response);
};
const renderTextWithNewlines = (text: string): React.ReactNode => text.split(/\\n|\n/).map((line, i) => (
<React.Fragment key={i}>
{i > 0 && <br />}
{line}
</React.Fragment>
));
const formatJson = (data: unknown): string => {
try {
return JSON.stringify(data, undefined, 2);
} catch (error) {
console.error('Error formatting JSON:', error);
return 'Error formatting data';
}
};
const formatTimestamp = (timestamp: number | undefined): string =>
timestamp ? new Date(timestamp).toLocaleString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}) : 'N/A';
export interface ExchangeCardProps {
exchange: LanguageModelExchange;
selectedAgentId?: string;
compactView?: boolean;
renderNewlines?: boolean;
}
export const ExchangeCard: React.FC<ExchangeCardProps> = ({ exchange, selectedAgentId, compactView = true, renderNewlines = false }) => {
const earliestTimestamp = exchange.requests.reduce((earliest, req) => {
const timestamp = req.metadata.timestamp as number || 0;
return timestamp && (!earliest || timestamp < earliest) ? timestamp : earliest;
}, 0);
return (
<div className="theia-card exchange-card"
role="article"
aria-label={`Exchange ${exchange.id}`}>
<div className='theia-card-meta'>
<span className='theia-card-request-id'>
{nls.localizeByDefault('ID')}: {exchange.id}
</span>
{exchange.metadata.agent && (
<span className='theia-card-agent-id'>
{nls.localize('theia/ai/history/exchange-card/agentId', 'Agent')}: {exchange.metadata.agent}
</span>
)}
</div>
<div className='theia-card-content'>
<div className='requests-container'>
{exchange.requests.map((request, index) => (
<RequestCard
key={request.id}
request={request}
index={index}
totalRequests={exchange.requests.length}
selectedAgentId={selectedAgentId}
compactView={compactView}
renderNewlines={renderNewlines}
/>
))}
</div>
</div>
<div className='theia-card-meta'>
{earliestTimestamp > 0 && (
<span className='theia-card-timestamp'>
{nls.localize('theia/ai/history/exchange-card/timestamp', 'Started')}: {formatTimestamp(earliestTimestamp)}
</span>
)}
</div>
</div>
);
};
interface RequestCardProps {
request: LanguageModelExchangeRequest;
index: number;
totalRequests: number;
selectedAgentId?: string;
compactView?: boolean;
renderNewlines?: boolean;
}
const RequestCard: React.FC<RequestCardProps> = ({ request, index, totalRequests, selectedAgentId, compactView = true, renderNewlines = false }) => {
const isFromDifferentAgent = selectedAgentId &&
request.metadata.agent &&
request.metadata.agent !== selectedAgentId;
const isStreamResponse = 'parts' in request.response;
const getRequestContent = () => {
if (compactView) {
const content = formatJson(request.request.messages);
return (
<div className="compact-response">
<pre className={`formatted-json ${renderNewlines ? 'render-newlines' : ''}`}>
{renderNewlines ? renderTextWithNewlines(content) : content}
</pre>
</div>
);
} else {
const content = formatJson(request.request);
return (
<pre className={`formatted-json ${renderNewlines ? 'render-newlines' : ''}`}>
{renderNewlines ? renderTextWithNewlines(content) : content}
</pre>
);
}
};
const getResponseContent = () => {
if (compactView) {
const content = getTextFromResponse(request.response);
return (
<div className="compact-response">
<pre className={`formatted-json ${renderNewlines ? 'render-newlines' : ''}`}>
{renderNewlines ? renderTextWithNewlines(content) : content}
</pre>
</div>
);
} else if (isStreamResponse) {
const streamResponse = request.response as LanguageModelMonitoredStreamResponse;
return streamResponse.parts.map((part, i) => (
<div key={`part-${i}`} className="stream-part">
<pre className={`formatted-json ${renderNewlines ? 'render-newlines' : ''}`}>
{renderNewlines ? renderTextWithNewlines(JSON.stringify(part, undefined, 2)) : JSON.stringify(part, undefined, 2)}
</pre>
</div>
));
} else {
const content = formatJson(request.response);
return (
<pre className={`formatted-json ${renderNewlines ? 'render-newlines' : ''}`}>
{renderNewlines ? renderTextWithNewlines(content) : content}
</pre>
);
}
};
return (
<div className={`request-card ${isFromDifferentAgent ? 'different-agent-opacity' : ''}`}>
<div className='request-header'>
{totalRequests > 1 && (
<h3>{nls.localize('theia/ai/history/request-card/title', 'Request')} {index + 1}</h3>
)}
<div className='request-info'>
<span className='request-id'>ID: {request.id}</span>
{request.metadata.agent && (
<span className={`request-agent ${isFromDifferentAgent ? 'different-agent-name' : ''}`}>
{nls.localize('theia/ai/history/request-card/agent', 'Agent')}: {request.metadata.agent}
</span>
)}
<span className='request-model'>
{nls.localize('theia/ai/history/request-card/model', 'Model')}: {request.languageModel}
</span>
{!!request.metadata.promptVariantId && (
<span className={`request-prompt-variant ${request.metadata.isPromptVariantCustomized ? 'customized' : ''}`}>
{!!request.metadata.isPromptVariantCustomized && (
<span className='customized-prefix'>
[{nls.localize('theia/ai/history/edited', 'edited')}]{' '}
</span>
)}
{nls.localize('theia/ai/history/request-card/promptVariant', 'Prompt Variant')}: {request.metadata.promptVariantId as string}
</span>
)}
</div>
</div>
<div className='request-content-container'>
<details>
<summary>
{nls.localize('theia/ai/history/request-card/request', 'Request')}
</summary>
<div className='request-content'>
{getRequestContent()}
</div>
</details>
<details>
<summary>
{nls.localize('theia/ai/history/request-card/response', 'Response')}
</summary>
<div className='response-content'>
{getResponseContent()}
</div>
</details>
</div>
<div className='request-meta'>
{request.metadata.timestamp && (
<span className='request-timestamp'>
{nls.localize('theia/ai/history/request-card/timestamp', 'Timestamp')}: {formatTimestamp(request.metadata.timestamp as number)}
</span>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,32 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { bindViewContribution, WidgetFactory } from '@theia/core/lib/browser';
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 => {
bindViewContribution(bind, AIHistoryViewContribution);
bind(AIHistoryView).toSelf();
bind(WidgetFactory).toDynamicValue(context => ({
id: AIHistoryView.ID,
createWidget: () => context.container.get<AIHistoryView>(AIHistoryView)
})).inSingletonScope();
bind(TabBarToolbarContribution).toService(AIHistoryViewContribution);
});

View File

@@ -0,0 +1,194 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Agent, AgentService, LanguageModelService, SessionEvent } from '@theia/ai-core';
import { LanguageModelExchange } from '@theia/ai-core/lib/common/language-model-interaction-model';
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 { ExchangeCard } from './ai-history-exchange-card';
import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component';
import { deepClone, nls } from '@theia/core';
namespace AIHistoryView {
export interface State {
chronological: boolean;
compactView: boolean;
renderNewlines: boolean;
selectedAgentId?: string;
}
}
@injectable()
export class AIHistoryView extends ReactWidget implements StatefulWidget {
@inject(LanguageModelService)
protected languageModelService: LanguageModelService;
@inject(AgentService)
protected readonly agentService: AgentService;
public static ID = 'ai-history-widget';
static LABEL = nls.localize('theia/ai/history/view/label', 'AI Agent History [Beta]');
protected _state: AIHistoryView.State = { chronological: false, compactView: true, renderNewlines: true };
constructor() {
super();
this.id = AIHistoryView.ID;
this.title.label = AIHistoryView.LABEL;
this.title.caption = AIHistoryView.LABEL;
this.title.closable = true;
this.title.iconClass = codicon('history');
}
protected get state(): AIHistoryView.State {
return this._state;
}
protected set state(state: AIHistoryView.State) {
this._state = state;
this.update();
}
storeState(): object {
return this.state;
}
restoreState(oldState: object & Partial<AIHistoryView.State>): void {
const copy = deepClone(this.state);
if (oldState.chronological !== undefined) {
copy.chronological = oldState.chronological;
}
if (oldState.compactView !== undefined) {
copy.compactView = oldState.compactView;
}
if (oldState.renderNewlines !== undefined) {
copy.renderNewlines = oldState.renderNewlines;
}
this.state = copy;
}
@postConstruct()
protected init(): void {
this.update();
this.toDispose.push(this.languageModelService.onSessionChanged((event: SessionEvent) => this.historyContentUpdated(event)));
this.selectAgent(this.agentService.getAllAgents()[0]);
}
protected selectAgent(agent: Agent | undefined): void {
this.state = { ...this.state, selectedAgentId: agent?.id };
}
protected historyContentUpdated(event: SessionEvent): void {
this.update();
}
render(): React.ReactNode {
const selectionChange = (value: SelectOption) => {
this.selectAgent(this.agentService.getAllAgents().find(agent => agent.id === value.value));
};
const agents = this.agentService.getAllAgents();
if (agents.length === 0) {
return (
<div className='agent-history-widget'>
<div className='theia-card no-content'>{nls.localize('theia/ai/history/view/noAgent', 'No agent available.')}</div>
</div >);
}
return (
<div className='agent-history-widget'>
<SelectComponent
options={agents.map(agent => ({
value: agent.id,
label: agent.name,
description: agent.description || ''
}))}
onChange={selectionChange}
defaultValue={this.state.selectedAgentId} />
<div className='agent-history'>
{this.renderHistory()}
</div>
</div>
);
}
protected renderHistory(): React.ReactNode {
if (!this.state.selectedAgentId) {
return <div className='theia-card no-content'>{nls.localize('theia/ai/history/view/noAgentSelected', 'No agent selected.')}</div>;
}
const exchanges = this.getExchangesByAgent(this.state.selectedAgentId);
if (exchanges.length === 0) {
const selectedAgent = this.agentService.getAllAgents().find(agent => agent.id === this.state.selectedAgentId);
return <div className='theia-card no-content'>
{nls.localize('theia/ai/history/view/noHistoryForAgent', 'No history available for the selected agent \'{0}\'', selectedAgent?.name || this.state.selectedAgentId)}
</div>;
}
// Sort exchanges by timestamp (using the first sub-request's timestamp)
const sortedExchanges = [...exchanges].sort((a, b) => {
const aTimestamp = a.requests[0]?.metadata.timestamp as number || 0;
const bTimestamp = b.requests[0]?.metadata.timestamp as number || 0;
return this.state.chronological ? aTimestamp - bTimestamp : bTimestamp - aTimestamp;
});
return sortedExchanges.map(exchange => (
<ExchangeCard
key={exchange.id}
exchange={exchange}
selectedAgentId={this.state.selectedAgentId}
compactView={this.state.compactView}
renderNewlines={this.state.renderNewlines}
/>
));
}
/**
* Get all exchanges for a specific agent.
* Includes all exchanges in which the agent is involved, either as the main exchange or as a sub-request.
* @param agentId The agent ID to filter by
*/
protected getExchangesByAgent(agentId: string): LanguageModelExchange[] {
return this.languageModelService.sessions.flatMap(session =>
session.exchanges.filter(exchange =>
exchange.metadata.agent === agentId ||
exchange.requests.some(request => request.metadata.agent === agentId)
)
);
}
public sortHistory(chronological: boolean): void {
this.state = { ...deepClone(this.state), chronological: chronological };
}
public toggleCompactView(): void {
this.state = { ...deepClone(this.state), compactView: !this.state.compactView };
}
public toggleRenderNewlines(): void {
this.state = { ...deepClone(this.state), renderNewlines: !this.state.renderNewlines };
}
get isChronological(): boolean {
return this.state.chronological === true;
}
get isCompactView(): boolean {
return this.state.compactView === true;
}
get isRenderNewlines(): boolean {
return this.state.renderNewlines === true;
}
}

View File

@@ -0,0 +1,237 @@
.agent-history-widget {
display: flex;
flex-direction: column;
align-items: center;
}
.agent-history-widget .theia-select-component {
margin: 10px 0;
width: 80%;
}
.agent-history {
width: calc(80% + 16px);
display: flex;
align-items: center;
flex-direction: column;
}
.theia-card {
background-color: var(--theia-sideBar-background);
border: 1px solid var(--theia-sideBarSectionHeader-border);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 15px;
margin: 10px 0;
width: 100%;
box-sizing: border-box;
}
.theia-card-meta {
display: flex;
justify-content: space-between;
font-size: 0.9em;
padding: var(--theia-ui-padding) 0;
}
.theia-card-content {
color: var(--theia-font-color);
margin-bottom: 10px;
}
.theia-card-content h3 {
font-size: var(--theia-ui-font-size1);
font-weight: bold;
}
.theia-card-content pre {
border-radius: 4px;
border: 1px solid var(--theia-sideBarSectionHeader-border);
background-color: var(--theia-terminal-background);
overflow: visible;
padding: 10px;
margin: 5px 0;
}
.theia-card-request-id,
.theia-card-timestamp {
flex: 1;
text-align: left;
}
.exchange-card {
border-radius: 6px;
transition: box-shadow 0.3s ease;
}
.requests-container {
display: flex;
flex-direction: column;
gap: 15px;
width: 100%;
overflow: visible;
}
/* Request Card Styles */
.request-card {
background-color: var(--theia-editor-background);
border: 1px solid var(--theia-sideBarSectionHeader-border);
border-radius: 4px;
padding: 12px;
margin-bottom: 15px;
transition: box-shadow 0.3s ease;
}
.request-header {
display: flex;
flex-direction: column;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid var(--theia-sideBarSectionHeader-border);
}
.request-header h3 {
margin: 0 0 8px 0;
}
.request-info {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.request-id,
.request-agent,
.request-model,
.request-prompt-variant {
font-size: 0.9em;
color: var(--theia-descriptionForeground);
white-space: nowrap;
padding: 2px 6px;
background-color: var(--theia-editor-inactiveSelectionBackground);
border-radius: 3px;
}
.request-prompt-variant.customized {
color: var(--theia-editorWarning-foreground);
background-color: var(--theia-inputValidation-warningBackground);
border: 1px solid var(--theia-editorWarning-foreground);
}
.request-content-container {
width: 100%;
overflow: visible;
}
.request-content-container details {
margin-bottom: 10px;
border: 1px solid var(--theia-editorWidget-border);
border-radius: 4px;
}
.request-content-container summary {
padding: 8px 12px;
background-color: var(--theia-editorWidget-background);
cursor: pointer;
font-weight: bold;
display: block; /* Make the entire summary clickable */
}
.request-content-container summary:hover {
background-color: var(--theia-list-hoverBackground);
}
.request-content,
.response-content {
padding: 10px;
overflow: visible;
}
.request-content-container details {
margin-bottom: 15px;
}
.request-content-container summary {
padding: 10px 15px;
font-size: 1em;
transition: background-color 0.2s ease;
}
.stream-part {
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px dashed var(--theia-sideBarSectionHeader-border);
}
.stream-part:last-child {
border-bottom: none;
}
.compact-response {
padding: 8px;
background-color: var(--theia-editor-background);
border-radius: 4px;
}
.formatted-json {
white-space: pre-wrap;
word-break: break-word;
font-family: var(--theia-ui-font-family-monospace);
font-size: var(--theia-code-font-size);
line-height: var(--theia-code-line-height);
color: var(--theia-editor-foreground);
}
.formatted-json.render-newlines {
white-space: pre-wrap;
}
.request-content-container summary::before {
content: '▶';
display: inline-block;
margin-right: 8px;
transition: transform 0.2s;
font-size: 0.8em;
}
.request-content-container details[open] summary::before {
transform: rotate(90deg);
}
.theia-card-agent-id {
flex: 1;
text-align: right;
font-weight: bold;
}
.request-card.different-agent-opacity {
opacity: 0.7;
}
.request-agent.different-agent-name {
font-style: italic;
color: var(--theia-notificationsWarningIcon-foreground);
}
/* Request meta information */
.request-meta {
display: flex;
justify-content: space-between;
font-size: 0.85em;
color: var(--theia-descriptionForeground);
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid var(--theia-sideBarSectionHeader-border);
}
.no-content {
padding: 15px;
color: var(--theia-descriptionForeground);
text-align: center;
font-style: italic;
}
.request-timestamp {
font-size: 0.85em;
color: var(--theia-descriptionForeground);
}

View File

@@ -0,0 +1,16 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* note: this bogus test file is required so that
we are able to run mocha unit tests on this
package, without having any actual unit tests in it.
This way a coverage report will be generated,
showing 0% coverage, instead of no report.
This file can be removed once we have real unit
tests in place. */
describe('ai-history package', () => {
it('support code coverage statistics', () => true);
});

View File

@@ -0,0 +1,31 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../ai-chat-ui"
},
{
"path": "../ai-core"
},
{
"path": "../core"
},
{
"path": "../filesystem"
},
{
"path": "../output"
},
{
"path": "../workspace"
}
]
}