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,35 @@
<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 - SCAN OSS AI EXTENSION</h2>
<hr />
</div>
## Description
Integrates SCANOSS content scanning into the Chat View.
Whenever a code listing is rendered, a scan action is offered to the user.
Via the preferences the user can switch between manual and automatic scanning.
## Additional Information
- [API documentation for `@theia/ai-scanoss`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_ai-scanoss.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
- [SCAN OSS Website](https://www.scanoss.com/)
## 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,55 @@
{
"name": "@theia/ai-scanoss",
"version": "1.68.0",
"description": "Theia - SCANOSS AI Integration",
"dependencies": {
"@theia/ai-chat": "1.68.0",
"@theia/ai-chat-ui": "1.68.0",
"@theia/ai-core": "1.68.0",
"@theia/core": "1.68.0",
"@theia/monaco": "1.68.0",
"@theia/monaco-editor-core": "1.96.302",
"@theia/scanoss": "1.68.0"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"frontend": "lib/browser/ai-scanoss-frontend-module",
"backend": "lib/node/ai-scanoss-backend-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"
],
"main": "lib/common",
"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,235 @@
// *****************************************************************************
// 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 { inject, injectable } from '@theia/core/shared/inversify';
import { CodeChatResponseContent } from '@theia/ai-chat';
import { CodePartRendererAction } from '@theia/ai-chat-ui/lib/browser/chat-response-renderer';
import {
ScanOSSResult,
ScanOSSResultMatch,
ScanOSSService,
} from '@theia/scanoss';
import { Dialog } from '@theia/core/lib/browser';
import { ReactNode } from '@theia/core/shared/react';
import { ResponseNode } from '@theia/ai-chat-ui/lib/browser/chat-tree-view';
import * as React from '@theia/core/shared/react';
import { ReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog';
import { SCAN_OSS_API_KEY_PREF } from '@theia/scanoss/lib/common/scanoss-preferences';
import { SCANOSS_MODE_PREF } from '../common/ai-scanoss-preferences';
import { nls, PreferenceService } from '@theia/core';
// cached map of scanOSS results.
// 'false' is stored when not automatic check is off and it was not (yet) requested deliberately.
type ScanOSSResults = Map<string, ScanOSSResult | false>;
interface HasScanOSSResults {
scanOSSResults: ScanOSSResults;
[key: string]: unknown;
}
function hasScanOSSResults(data: {
[key: string]: unknown;
}): data is HasScanOSSResults {
return 'scanOSSResults' in data && data.scanOSSResults instanceof Map;
}
@injectable()
export class ScanOSSScanButtonAction implements CodePartRendererAction {
@inject(ScanOSSService)
protected readonly scanService: ScanOSSService;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
priority = 0;
canRender(response: CodeChatResponseContent, parentNode: ResponseNode): boolean {
if (!hasScanOSSResults(parentNode.response.data)) {
parentNode.response.data.scanOSSResults = new Map<
string,
ScanOSSResult
>();
}
const results = parentNode.response.data
.scanOSSResults as ScanOSSResults;
const scanOSSMode = this.preferenceService.get(SCANOSS_MODE_PREF, 'off');
// we mark the code for manual scanning in case it was not handled yet and the mode is manual or off.
// this prevents a possibly unexpected automatic scan of "old" snippets if automatic scan is later turned on.
if (results.get(response.code) === undefined && (scanOSSMode === 'off' || scanOSSMode === 'manual')) {
results.set(response.code, false);
}
return scanOSSMode !== 'off';
}
render(
response: CodeChatResponseContent,
parentNode: ResponseNode
): ReactNode {
const scanOSSResults = parentNode.response.data
.scanOSSResults as ScanOSSResults;
return (
<ScanOSSIntegration
key='scanoss'
code={response.code}
scanService={this.scanService}
scanOSSResults={scanOSSResults}
preferenceService={this.preferenceService}
/>
);
}
}
const ScanOSSIntegration = React.memo((props: {
code: string;
scanService: ScanOSSService;
scanOSSResults: ScanOSSResults;
preferenceService: PreferenceService;
}) => {
const [automaticCheck] = React.useState(() =>
props.preferenceService.get<string>(SCANOSS_MODE_PREF, 'off') === 'automatic'
);
const [scanOSSResult, setScanOSSResult] = React.useState<
ScanOSSResult | 'pending' | undefined | false
>(props.scanOSSResults.get(props.code));
const scanCode = React.useCallback(async () => {
setScanOSSResult('pending');
const result = await props.scanService.scanContent(props.code, props.preferenceService.get(SCAN_OSS_API_KEY_PREF, undefined));
setScanOSSResult(result);
props.scanOSSResults.set(props.code, result);
return result;
}, [props.code, props.scanService]);
React.useEffect(() => {
if (scanOSSResult === undefined) {
if (automaticCheck) {
scanCode();
} else {
// sanity fallback. This codepath should already be handled via "canRender"
props.scanOSSResults.set(props.code, false);
}
}
}, []);
const scanOSSClicked = React.useCallback(async () => {
let scanResult = scanOSSResult;
if (scanResult === 'pending') {
return;
}
if (!scanResult || scanResult.type === 'error') {
scanResult = await scanCode();
}
if (scanResult && scanResult.type === 'match') {
const dialog = new ScanOSSDialog([scanResult]);
dialog.open();
}
}, [scanOSSResult]);
let title = 'SCANOSS - Perform scan';
if (scanOSSResult) {
if (scanOSSResult === 'pending') {
title = nls.localize('theia/ai/scanoss/snippet/in-progress', 'SCANOSS - Performing scan...');
} else if (scanOSSResult.type === 'error') {
title = nls.localize('theia/ai/scanoss/snippet/errored', 'SCANOSS - Error - {0}', scanOSSResult.message);
} else if (scanOSSResult.type === 'match') {
title = nls.localize('theia/ai/scanoss/snippet/matched', 'SCANOSS - Found {0} match', scanOSSResult.matched);
} else if (scanOSSResult.type === 'clean') {
title = nls.localize('theia/ai/scanoss/snippet/no-match', 'SCANOSS - No match');
}
}
return (
<div
className={`button scanoss-icon icon-container ${scanOSSResult === 'pending'
? 'pending'
: scanOSSResult
? scanOSSResult.type
: ''
}`}
title={title}
role="button"
onClick={scanOSSClicked}
>
{scanOSSResult && scanOSSResult !== 'pending' && (
<span className="status-icon">
{scanOSSResult.type === 'clean' && <span className="codicon codicon-pass-filled" />}
{scanOSSResult.type === 'match' && <span className="codicon codicon-warning" />}
{scanOSSResult.type === 'error' && <span className="codicon codicon-error" />}
</span>
)}
</div>
);
});
export class ScanOSSDialog extends ReactDialog<void> {
protected readonly okButton: HTMLButtonElement;
constructor(protected results: ScanOSSResultMatch[]) {
super({
title: nls.localize('theia/ai/scanoss/snippet/dialog-header', 'ScanOSS Results'),
});
this.appendAcceptButton(Dialog.OK);
this.update();
}
protected render(): React.ReactNode {
return (
<div className="scanoss-dialog-container">
{this.renderHeader()}
{this.renderSummary()}
{this.renderContent()}
</div>
);
}
protected renderHeader(): React.ReactNode {
return (
<div className="scanoss-header">
<div className="scanoss-icon-container">
<div className="scanoss-icon"></div>
<h2>SCANOSS</h2>
</div>
</div>
);
}
protected renderSummary(): React.ReactNode {
return (
<div className="scanoss-summary">
<h3>{nls.localize('theia/ai/scanoss/snippet/summary', 'Summary')}</h3>
<div>
{nls.localize('theia/ai/scanoss/snippet/match-count', 'Found {0} match(es)', this.results.length)}
</div>
</div>
);
}
protected renderContent(): React.ReactNode {
return (
<div className="scanoss-details">
<h4>{nls.localizeByDefault('Details')}</h4>
{this.results.map(result =>
<div key={result.matched}>
{result.file && <h4>{nls.localize('theia/ai/scanoss/snippet/file-name-heading', 'Match found in {0}', result.file)}</h4>}
<a href={result.url} target="_blank" rel="noopener noreferrer">
{result.url}
</a>
<pre>
{JSON.stringify(result.raw, undefined, 2)}
</pre>
</div>)}
</div>
);
}
get value(): undefined {
return undefined;
}
}

View File

@@ -0,0 +1,36 @@
// *****************************************************************************
// 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 '../../src/browser/style/index.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import { AIScanOSSPreferencesSchema } from '../common/ai-scanoss-preferences';
import { ScanOSSScanButtonAction } from './ai-scanoss-code-scan-action';
import { CodePartRendererAction } from '@theia/ai-chat-ui/lib/browser/chat-response-renderer';
import { ChangeSetActionRenderer } from '@theia/ai-chat-ui/lib/browser/change-set-actions/change-set-action-service';
import { ChangeSetScanActionRenderer } from './change-set-scan-action/change-set-scan-action';
import { ChangeSetDecorator } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
import { ChangeSetScanDecorator } from './change-set-scan-action/change-set-scan-decorator';
import { PreferenceContribution } from '@theia/core';
export default new ContainerModule(bind => {
bind(PreferenceContribution).toConstantValue({ schema: AIScanOSSPreferencesSchema });
bind(ScanOSSScanButtonAction).toSelf().inSingletonScope();
bind(CodePartRendererAction).toService(ScanOSSScanButtonAction);
bind(ChangeSetScanActionRenderer).toSelf();
bind(ChangeSetActionRenderer).toService(ChangeSetScanActionRenderer);
bind(ChangeSetScanDecorator).toSelf().inSingletonScope();
bind(ChangeSetDecorator).toService(ChangeSetScanDecorator);
});

View File

@@ -0,0 +1,286 @@
// *****************************************************************************
// 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
// *****************************************************************************
import * as React from '@theia/core/shared/react';
import { ChangeSet, ChangeSetElement } from '@theia/ai-chat';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { ChangeSetActionRenderer } from '@theia/ai-chat-ui/lib/browser/change-set-actions/change-set-action-service';
import { PreferenceService } from '@theia/core/lib/common/preferences';
import { ScanOSSService, ScanOSSResult, ScanOSSResultMatch } from '@theia/scanoss';
import { SCANOSS_MODE_PREF } from '../../common/ai-scanoss-preferences';
import { SCAN_OSS_API_KEY_PREF } from '@theia/scanoss/lib/common/scanoss-preferences';
import { ChangeSetFileElement } from '@theia/ai-chat/lib/browser/change-set-file-element';
import { ScanOSSDialog } from '../ai-scanoss-code-scan-action';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { IDiffProviderFactoryService } from '@theia/monaco-editor-core/esm/vs/editor/browser/widget/diffEditor/diffProviderFactoryService';
import { IDocumentDiffProvider } from '@theia/monaco-editor-core/esm/vs/editor/common/diff/documentDiffProvider';
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { CancellationToken, Emitter, MessageService, nls } from '@theia/core';
import { ChangeSetScanDecorator } from './change-set-scan-decorator';
import { AIActivationService } from '@theia/ai-core/lib/browser';
type ScanOSSState = 'pending' | 'clean' | 'match' | 'error' | 'none';
type ScanOSSResultOptions = 'pending' | ScanOSSResult[] | undefined;
@injectable()
export class ChangeSetScanActionRenderer implements ChangeSetActionRenderer {
readonly id = 'change-set-scanoss';
readonly priority = 10;
protected readonly onDidChangeEmitter = new Emitter<void>();
readonly onDidChange = this.onDidChangeEmitter.event;
@inject(ScanOSSService)
protected readonly scanService: ScanOSSService;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(MonacoTextModelService)
protected readonly textModelService: MonacoTextModelService;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(ChangeSetScanDecorator)
protected readonly scanChangeSetDecorator: ChangeSetScanDecorator;
@inject(AIActivationService)
protected readonly activationService: AIActivationService;
protected differ: IDocumentDiffProvider;
@postConstruct()
init(): void {
this.differ = StandaloneServices.get(IDiffProviderFactoryService).createDiffProvider({ diffAlgorithm: 'advanced' });
this._scan = this.runScan.bind(this);
this.preferenceService.onPreferenceChanged(e => e.affects(SCANOSS_MODE_PREF) && this.onDidChangeEmitter.fire());
}
canRender(): boolean {
return this.activationService.isActive;
}
render(changeSet: ChangeSet): React.ReactNode {
return (
<ChangeSetScanOSSIntegration
changeSet={changeSet}
decorator={this.scanChangeSetDecorator}
scanOssMode={this.getPreferenceValues()}
scanChangeSet={this._scan}
/>
);
}
protected getPreferenceValues(): string {
return this.preferenceService.get(SCANOSS_MODE_PREF, 'off');
}
protected _scan: (changeSetElements: ChangeSetElement[]) => Promise<ScanOSSResult[]>;
protected async runScan(changeSetElements: ChangeSetFileElement[], cache: Map<string, ScanOSSResult>, userTriggered: boolean): Promise<ScanOSSResult[]> {
const apiKey = this.preferenceService.get(SCAN_OSS_API_KEY_PREF, undefined);
let notifiedError = false;
const fileResults = await Promise.all(changeSetElements.map(async fileChange => {
if (fileChange.targetState.trim().length === 0) {
return { type: 'clean' } satisfies ScanOSSResult;
}
const toScan = await this.getScanContent(fileChange);
if (!toScan) { return { type: 'clean' } satisfies ScanOSSResult; }
const cached = cache.get(toScan);
if (cached) { return cached; }
const result = { ...await this.scanService.scanContent(toScan, apiKey), file: fileChange.uri.path.toString() };
if (result.type !== 'error') {
cache.set(toScan, result);
} else if (!notifiedError && userTriggered) {
notifiedError = true;
this.messageService.warn(nls.localize('theia/ai/scanoss/changeSet/error-notification', 'ScanOSS error encountered: {0}.', result.message));
}
return result;
}));
return fileResults;
}
protected async getScanContent(fileChange: ChangeSetFileElement): Promise<string> {
if (fileChange.replacements) {
return fileChange.replacements.map(({ newContent }) => newContent).join('\n\n').trim();
}
const textModels = await Promise.all([
this.textModelService.createModelReference(fileChange.uri),
this.textModelService.createModelReference(fileChange.changedUri)
]);
const [original, changed] = textModels;
const diff = await this.differ.computeDiff(
original.object.textEditorModel,
changed.object.textEditorModel,
{ maxComputationTimeMs: 5000, computeMoves: false, ignoreTrimWhitespace: true },
CancellationToken.None
);
if (diff.identical) { return ''; }
const insertions = diff.changes.filter(candidate => !candidate.modified.isEmpty);
if (insertions.length === 0) { return ''; }
const changedLinesInSuggestion = insertions.map(change => {
const range = change.modified.toInclusiveRange();
return range ? changed.object.textEditorModel.getValueInRange(range) : ''; // In practice, we've filtered out cases where the range would be null already.
}).join('\n\n');
textModels.forEach(ref => ref.dispose());
return changedLinesInSuggestion.trim();
}
}
interface ChangeSetScanActionProps {
changeSet: ChangeSet;
decorator: ChangeSetScanDecorator;
scanOssMode: string;
scanChangeSet: (changeSet: ChangeSetElement[], cache: Map<string, ScanOSSResult>, userTriggered: boolean) => Promise<ScanOSSResult[]>
}
const ChangeSetScanOSSIntegration = React.memo(({
changeSet,
decorator,
scanOssMode,
scanChangeSet
}: ChangeSetScanActionProps) => {
const [scanOSSResult, setScanOSSResult] = React.useState<ScanOSSResult[] | 'pending' | undefined>(undefined);
const cache = React.useRef(new Map<string, ScanOSSResult>());
const [changeSetElements, setChangeSetElements] = React.useState(() => changeSet.getElements().filter(candidate => candidate instanceof ChangeSetFileElement));
React.useEffect(() => {
if (scanOSSResult === undefined) {
if (scanOssMode === 'automatic' && scanOSSResult === undefined) {
setScanOSSResult('pending');
scanChangeSet(changeSetElements, cache.current, false).then(result => setScanOSSResult(result));
}
}
}, [scanOssMode, scanOSSResult]);
React.useEffect(() => {
if (!Array.isArray(scanOSSResult)) {
decorator.setScanResult([]);
return;
}
decorator.setScanResult(scanOSSResult);
}, [decorator, scanOSSResult]);
React.useEffect(() => {
const disposable = changeSet.onDidChange(() => {
setChangeSetElements(changeSet.getElements().filter(candidate => candidate instanceof ChangeSetFileElement));
setScanOSSResult(undefined);
});
return () => disposable.dispose();
}, [changeSet]);
const scanOSSClicked = React.useCallback(async () => {
if (scanOSSResult === 'pending') {
return;
} else if (!scanOSSResult || scanOSSResult.some(candidate => candidate.type === 'error')) {
setScanOSSResult('pending');
scanChangeSet(changeSetElements, cache.current, true).then(result => setScanOSSResult(result));
} else {
const matches = scanOSSResult.filter((candidate): candidate is ScanOSSResultMatch => candidate.type === 'match');
if (matches.length === 0) { return; }
const dialog = new ScanOSSDialog(matches);
dialog.open();
}
}, [scanOSSResult, changeSetElements]);
const state = getResult(scanOSSResult);
const title = `ScanOSS: ${getTitle(state)}`;
const content = getContent(state);
const icon = getIcon(state);
if (scanOssMode === 'off' || changeSetElements.length === 0) {
return undefined;
} else if (state === 'clean' || state === 'pending') {
return <div className='theia-changeSet-scanOss readonly'>
<div
className={`scanoss-icon icon-container ${state === 'pending'
? 'pending'
: state
? state
: ''
}`}
title={title}
>
{icon}
</div>
</div>;
} else {
return <button
className={`theia-button secondary theia-changeSet-scanOss ${state}`}
title={title}
onClick={scanOSSClicked}
>
<div
className={`scanoss-icon icon-container ${state}`}
title={title}
>
{icon}
</div>
{content}
</button>;
}
});
function getResult(scanOSSResult: ScanOSSResultOptions): ScanOSSState {
switch (true) {
case scanOSSResult === undefined: return 'none';
case scanOSSResult === 'pending': return 'pending';
case (scanOSSResult as ScanOSSResult[]).some(candidate => candidate.type === 'error'): return 'error';
case (scanOSSResult as ScanOSSResult[]).some(candidate => candidate.type === 'match'): return 'match';
default: return 'clean';
}
}
function getTitle(result: ScanOSSState): string {
switch (result) {
case 'none': return nls.localize('theia/ai/scanoss/changeSet/scan', 'Scan');
case 'pending': return nls.localize('theia/ai/scanoss/changeSet/scanning', 'Scanning...');
case 'error': return nls.localize('theia/ai/scanoss/changeSet/error', 'Error: Rerun');
case 'match': return nls.localize('theia/ai/scanoss/changeSet/match', 'View Matches');
case 'clean': return nls.localize('theia/ai/scanoss/changeSet/clean', 'No Matches');
default: return '';
}
}
function getContent(result: ScanOSSState): string {
switch (result) {
case 'none': return getTitle(result);
case 'pending': return getTitle(result);
default: return '';
}
}
function getIcon(result: ScanOSSState): React.ReactNode {
switch (result) {
case 'clean': return (<span className="status-icon">
<span className="codicon codicon-pass-filled" />
</span>);
case 'match': return (<span className="status-icon">
<span className="codicon codicon-warning" />
</span>);
default: return undefined;
}
}

View File

@@ -0,0 +1,54 @@
// *****************************************************************************
// 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
// *****************************************************************************
import type { ChangeSetDecoration, ChangeSetElement } from '@theia/ai-chat';
import type { ChangeSetDecorator } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
import { Emitter } from '@theia/core';
import { injectable } from '@theia/core/shared/inversify';
import type { ScanOSSResult } from '@theia/scanoss';
@injectable()
export class ChangeSetScanDecorator implements ChangeSetDecorator {
readonly id = 'thei-change-set-scanoss-decorator';
protected readonly emitter = new Emitter<void>();
readonly onDidChangeDecorations = this.emitter.event;
protected scanResult: ScanOSSResult[] = [];
setScanResult(results: ScanOSSResult[]): void {
this.scanResult = results;
this.emitter.fire();
}
decorate(element: ChangeSetElement): ChangeSetDecoration | undefined {
const match = this.scanResult.find(result => {
if (result.type === 'match') {
return result.file === element.uri.path.toString();
}
return false;
});
if (match) {
return {
additionalInfoSuffixIcon: ['additional-info-scanoss-icon', 'match', 'codicon', 'codicon-warning'],
};
}
return undefined;
}
}

View File

@@ -0,0 +1,201 @@
.scanoss-icon::before {
content: "";
mask-image: url("scanoss_logo_dark_theme.svg");
mask-repeat: no-repeat;
mask-position: 50% 50%;
mask-size: inherit;
height: inherit;
width: inherit;
display: inline-block;
background: var(--theia-sideBar-foreground, var(--theia-foreground));
}
.vs-light .scanoss-icon::before,
.theia-light .scanoss-icon::before,
.light-theia .scanoss-icon::before {
mask-image: url("scanoss_logo_light_theme.svg");
}
/* We need a more detailed selector to override the default button style */
.theia-CodePartRenderer-actions .scanoss-icon::before {
transition: filter 0.3s;
}
.scanoss-icon.pending {
pointer-events: none;
cursor: default;
}
.scanoss-icon.pending::before {
animation: scan-os-fade 3s infinite ease-in-out;
}
@keyframes scan-os-fade {
0%,
100% {
opacity: 0;
}
50% {
opacity: 1;
}
}
/* Use a rounded background when not hovered */
.theia-CodePartRenderer-actions .scanoss-icon:not(:hover) {
border-radius: 50%;
}
.scanoss-icon.match::before,
.scanoss-icon.clean::before,
.scanoss-icon.error::before {
background-color: var(--theia-disabledForeground);
}
.icon-container {
position: relative;
display: inline-block;
width: 16px;
height: 16px;
}
.icon-container.scanoss-icon::before {
width: 16px;
height: 16px;
mask-size: 16px;
}
.icon-container.scanoss-icon.clean,
.icon-container.scanoss-icon.match,
.icon-container.scanoss-icon.error {
margin-right: 12px;
}
/* The placeholder is used to align with the remaining actions, however it should not be visible*/
.scanoss-icon .placeholder {
visibility: hidden;
}
/* The status icon is used to display the result of the scan on the top right */
.scanoss-icon .status-icon {
position: absolute;
font-size: 14px;
transform: translate(-4px, -2px);
}
.scanoss-icon.clean .status-icon {
color: var(--theia-successBackground);
}
.scanoss-icon.match .status-icon,
.additional-info-scanoss-icon.match {
color: var(--theia-problemsWarningIcon-foreground);
}
.scanoss-icon.error .status-icon {
color: var(--theia-problemsErrorIcon-foreground);
}
/* Disable the button when it's in the clean state */
.theia-CodePartRenderer-actions .scanoss-icon.clean {
pointer-events: none;
cursor: default;
}
.scanoss-dialog-container .scanoss-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
.scanoss-dialog-container .scanoss-icon-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
.scanoss-dialog-container .scanoss-icon {
width: 40px;
height: 40px;
mask-size: 40px;
}
.scanoss-dialog-container .scanoss-icon-container h2 {
font-size: 1.8em;
margin: 0;
}
.scanoss-dialog-container .scanoss-summary {
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.scanoss-dialog-container .scanoss-summary h3 {
margin-top: 0;
font-size: 1.4em;
margin-bottom: 10px;
}
.scanoss-dialog-container .scanoss-details {
padding: 15px;
border-radius: 4px;
border: 1px solid var(--theia-sideBarSectionHeader-border);
}
.scanoss-dialog-container .scanoss-details h4 {
margin-top: 0;
font-size: 1.2em;
margin-bottom: 10px;
}
.scanoss-dialog-container .scanoss-details pre {
background: var(--theia-editor-background);
padding: 10px;
border-radius: 4px;
max-width: 70vw;
max-height: 40vh;
overflow: auto;
font-size: 0.9em;
}
/* ####### Change Set ####### */
.theia-changeSet-scanOss {
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: center;
gap: var(--theia-ui-padding);
}
.theia-changeSet-scanOss.readonly {
margin-right: 10px;
height: 100%;
}
.theia-changeSet-Action .theia-button.secondary {
background: none;
}
.theia-changeSet-Action .theia-button.secondary:hover {
background: var(--theia-toolbar-hoverBackground);
}
.theia-changeSet-scanOss.clean,
.theia-changeSet-scanOss.pending {
cursor: default;
}
.theia-changeSet-scanOss.clean:hover,
.theia-changeSet-scanOss.pending:hover {
background-color: var(--theia-button-background);
}
.theia-changeSet-scanOss .scanoss-icon.clean,
.theia-changeSet-scanOss .scanoss-icon.match,
.theia-changeSet-scanOss .scanoss-icon.error {
margin-right: 0px;
}
.theia-changeSet-scanOss .status-icon {
height: 16px;
}

View File

@@ -0,0 +1,19 @@
<svg width="81" height="82" viewBox="0 0 81 82" fill="none" xmlns="http://www.w3.org/2000/svg"
stroke-width="1">
<path
d="M18.5086 36.073H29.0966C30.2492 33.5499 32.2272 31.4981 34.7021 30.2582C37.1771 29.0184 40.0003 28.6651 42.7029 29.257C45.4056 29.8488 47.8253 31.3502 49.5603 33.512C51.2953 35.6738 52.2414 38.3659 52.2414 41.1415C52.2414 43.9171 51.2953 46.6092 49.5603 48.771C47.8253 50.9328 45.4056 52.4342 42.7029 53.0261C40.0003 53.6179 37.1771 53.2646 34.7021 52.0248C32.2272 50.7849 30.2492 48.7332 29.0966 46.21H18.5086C19.7483 51.5397 22.898 56.226 27.3581 59.3766C31.8182 62.5273 37.2773 63.9225 42.6961 63.2964C48.1149 62.6704 53.1152 60.0669 56.7449 55.9816C60.3746 51.8963 62.3804 46.6144 62.3804 41.1415C62.3804 35.6686 60.3746 30.3867 56.7449 26.3014C53.1152 22.2161 48.1149 19.6126 42.6961 18.9866C37.2773 18.3606 31.8182 19.7557 27.3581 22.9064C22.898 26.057 19.7483 30.7433 18.5086 36.073Z"
fill="white" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M40.412 10.1933C23.3992 10.1933 9.60766 24.0311 9.60766 41.101C9.60766 58.1708 23.3992 72.0086 40.412 72.0086C57.4247 72.0086 71.2163 58.1708 71.2163 41.101C71.2163 24.0311 57.4247 10.1933 40.412 10.1933ZM0.0908203 41.101C0.0908203 18.7575 18.1432 0.644531 40.412 0.644531C62.6808 0.644531 80.7332 18.7575 80.7332 41.101C80.7332 63.4444 62.6808 81.5574 40.412 81.5574C18.1432 81.5574 0.0908203 63.4444 0.0908203 41.101Z"
fill="white" />
<path
d="M40.1112 46.015C42.666 46.015 44.7371 43.9737 44.7371 41.4557C44.7371 38.9377 42.666 36.8965 40.1112 36.8965C37.5564 36.8965 35.4854 38.9377 35.4854 41.4557C35.4854 43.9737 37.5564 46.015 40.1112 46.015Z"
fill="url(#paint0_linear_458_1146)" />
<defs>
<linearGradient id="paint0_linear_458_1146" x1="35.4854" y1="63.6006" x2="45.2955"
y2="63.4776" gradientUnits="userSpaceOnUse">
<stop stop-color="#6366F1" />
<stop offset="1" stop-color="#863DFF" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,11 @@
<svg width="82" height="82" viewBox="0 0 82 82" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.1814 36.073H29.7694C30.9221 33.5499 32.9 31.4981 35.375 30.2582C37.85 29.0184 40.6731 28.6651 43.3758 29.257C46.0785 29.8488 48.4981 31.3502 50.2332 33.512C51.9682 35.6738 52.9142 38.3659 52.9142 41.1415C52.9142 43.9171 51.9682 46.6092 50.2332 48.771C48.4981 50.9328 46.0785 52.4342 43.3758 53.0261C40.6731 53.6179 37.85 53.2646 35.375 52.0248C32.9 50.7849 30.9221 48.7332 29.7694 46.21H19.1814C20.4211 51.5397 23.5708 56.226 28.0309 59.3766C32.491 62.5273 37.9501 63.9225 43.3689 63.2964C48.7878 62.6704 53.788 60.0669 57.4177 55.9816C61.0475 51.8963 63.0532 46.6144 63.0532 41.1415C63.0532 35.6686 61.0475 30.3867 57.4177 26.3014C53.788 22.2161 48.7878 19.6126 43.3689 18.9866C37.9501 18.3606 32.491 19.7557 28.0309 22.9064C23.5708 26.057 20.4211 30.7433 19.1814 36.073Z" fill="#1E293B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.0848 10.1933C24.0721 10.1933 10.2805 24.0311 10.2805 41.101C10.2805 58.1708 24.072 72.0086 41.0848 72.0086C58.0976 72.0086 71.8892 58.1708 71.8892 41.101C71.8892 24.0311 58.0976 10.1933 41.0848 10.1933ZM0.763672 41.101C0.763672 18.7575 18.816 0.644531 41.0848 0.644531C63.3536 0.644531 81.406 18.7575 81.406 41.101C81.406 63.4444 63.3536 81.5574 41.0848 81.5574C18.816 81.5574 0.763672 63.4444 0.763672 41.101Z" fill="#1E293B"/>
<path d="M40.7841 46.015C43.3389 46.015 45.41 43.9737 45.41 41.4557C45.41 38.9377 43.3389 36.8965 40.7841 36.8965C38.2293 36.8965 36.1582 38.9377 36.1582 41.4557C36.1582 43.9737 38.2293 46.015 40.7841 46.015Z" fill="url(#paint0_linear_458_1134)"/>
<defs>
<linearGradient id="paint0_linear_458_1134" x1="36.1582" y1="63.6006" x2="45.9684" y2="63.4776" gradientUnits="userSpaceOnUse">
<stop stop-color="#6366F1"/>
<stop offset="1" stop-color="#863DFF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,39 @@
// *****************************************************************************
// 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 { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/common/ai-core-preferences';
import { nls, PreferenceSchema } from '@theia/core';
export const SCANOSS_MODE_PREF = 'ai-features.SCANOSS.mode';
export const AIScanOSSPreferencesSchema: PreferenceSchema = {
properties: {
[SCANOSS_MODE_PREF]: {
type: 'string',
enum: ['off', 'manual', 'automatic'],
markdownEnumDescriptions: [
nls.localize('theia/ai/scanoss/mode/off/description', 'Feature is turned off completely.'),
nls.localize('theia/ai/scanoss/mode/manual/description', 'User can manually trigger the scan by clicking the SCANOSS item in the chat view.'),
nls.localize('theia/ai/scanoss/mode/automatic/description', 'Enable automatic scan of code snippets in chat views.')
],
markdownDescription: nls.localize('theia/ai/scanoss/mode/description',
'Configure the SCANOSS feature for analyzing code snippets in chat views. This will send a hash of suggested code snippets to the SCANOSS\n\
service hosted by the [Software Transparency foundation](https://www.softwaretransparency.org/osskb) for analysis.'),
default: 'off',
title: AI_CORE_PREFERENCES_TITLE
}
}
};

View File

@@ -0,0 +1,23 @@
// *****************************************************************************
// 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 { ContainerModule } from '@theia/core/shared/inversify';
import { AIScanOSSPreferencesSchema } from '../common/ai-scanoss-preferences';
import { PreferenceContribution } from '@theia/core';
export default new ContainerModule(bind => {
bind(PreferenceContribution).toConstantValue({ schema: AIScanOSSPreferencesSchema });
});

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2024 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-scanoss 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"
},
{
"path": "../ai-chat-ui"
},
{
"path": "../ai-core"
},
{
"path": "../core"
},
{
"path": "../monaco"
},
{
"path": "../scanoss"
}
]
}