Skip to content

Commit 0b3f538

Browse files
Fix ScanOSS button/icon in chat ui (#15339)
Co-authored-by: Philip Langer <planger@eclipsesource.com>
1 parent 7efd7de commit 0b3f538

File tree

12 files changed

+387
-127
lines changed

12 files changed

+387
-127
lines changed

packages/ai-chat-ui/src/browser/chat-input-widget.tsx

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { AIVariableResolutionRequest } from '@theia/ai-core';
2727
import { FrontendVariableService } from '@theia/ai-core/lib/browser';
2828
import { ContextVariablePicker } from './context-variable-picker';
2929
import { ChangeSetActionRenderer, ChangeSetActionService } from './change-set-actions/change-set-action-service';
30+
import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
3031

3132
type Query = (query: string) => Promise<void>;
3233
type Unpin = () => void;
@@ -70,6 +71,9 @@ export class AIChatInputWidget extends ReactWidget {
7071
@inject(ChangeSetActionService)
7172
protected readonly changeSetActionService: ChangeSetActionService;
7273

74+
@inject(ChangeSetDecoratorService)
75+
protected readonly changeSetDecoratorService: ChangeSetDecoratorService;
76+
7377
protected editorRef: MonacoEditor | undefined = undefined;
7478
protected readonly editorReady = new Deferred<void>();
7579

@@ -169,6 +173,7 @@ export class AIChatInputWidget extends ReactWidget {
169173
showChangeSet={this.configuration?.showChangeSet}
170174
labelProvider={this.labelProvider}
171175
actionService={this.changeSetActionService}
176+
decoratorService={this.changeSetDecoratorService}
172177
initialValue={this._initialValue}
173178
/>
174179
);
@@ -268,6 +273,7 @@ interface ChatInputProperties {
268273
showChangeSet?: boolean;
269274
labelProvider: LabelProvider;
270275
actionService: ChangeSetActionService;
276+
decoratorService: ChangeSetDecoratorService;
271277
initialValue?: string;
272278
}
273279

@@ -282,6 +288,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
282288
? buildChangeSetUI(
283289
props.chatModel.changeSet,
284290
props.labelProvider,
291+
props.decoratorService,
285292
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
286293
onDeleteChangeSet,
287294
onDeleteChangeSetElement
@@ -401,6 +408,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
401408
setChangeSetUI(buildChangeSetUI(
402409
event.changeSet,
403410
props.labelProvider,
411+
props.decoratorService,
404412
props.actionService.getActionsForChangeset(event.changeSet),
405413
onDeleteChangeSet,
406414
onDeleteChangeSetElement
@@ -412,6 +420,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
412420
? buildChangeSetUI(
413421
props.chatModel.changeSet,
414422
props.labelProvider,
423+
props.decoratorService,
415424
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
416425
onDeleteChangeSet,
417426
onDeleteChangeSetElement
@@ -433,6 +442,23 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
433442
return () => disposable.dispose();
434443
});
435444

445+
React.useEffect(() => {
446+
const disposable = props.decoratorService.onDidChangeDecorations(() => {
447+
if (!props.chatModel.changeSet) {
448+
return;
449+
}
450+
setChangeSetUI(buildChangeSetUI(
451+
props.chatModel.changeSet,
452+
props.labelProvider,
453+
props.decoratorService,
454+
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
455+
onDeleteChangeSet,
456+
onDeleteChangeSetElement
457+
));
458+
});
459+
return () => disposable.dispose();
460+
});
461+
436462
const setValue = React.useCallback((value: string) => {
437463
if (editorRef.current && !editorRef.current.document.isDisposed()) {
438464
editorRef.current.document.textEditorModel.setValue(value);
@@ -570,6 +596,7 @@ const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
570596
const buildChangeSetUI = (
571597
changeSet: ChangeSet,
572598
labelProvider: LabelProvider,
599+
decoratorService: ChangeSetDecoratorService,
573600
actions: ChangeSetActionRenderer[],
574601
onDeleteChangeSet: () => void,
575602
onDeleteChangeSetElement: (index: number) => void
@@ -583,11 +610,12 @@ const buildChangeSetUI = (
583610
nameClass: `${element.type} ${element.state}`,
584611
name: element.name ?? labelProvider.getName(element.uri),
585612
additionalInfo: element.additionalInfo ?? labelProvider.getDetails(element.uri),
613+
additionalInfoSuffixIcon: decoratorService.getAdditionalInfoSuffixIcon(element),
586614
openChange: element?.openChange?.bind(element),
587615
apply: element.state !== 'applied' ? element?.apply?.bind(element) : undefined,
588616
revert: element.state === 'applied' || element.state === 'stale' ? element?.revert?.bind(element) : undefined,
589617
delete: () => onDeleteChangeSetElement(changeSet.getElements().indexOf(element))
590-
})),
618+
} satisfies ChangeSetUIElement)),
591619
actions
592620
});
593621

@@ -596,6 +624,7 @@ interface ChangeSetUIElement {
596624
iconClass: string;
597625
nameClass: string;
598626
additionalInfo: string;
627+
additionalInfoSuffixIcon?: string[];
599628
open?: () => void;
600629
openChange?: () => void;
601630
apply?: () => void;
@@ -625,15 +654,18 @@ const ChangeSetBox: React.FunctionComponent<{ changeSet: ChangeSetUI }> = React.
625654
<ul>
626655
{elements.map((element, index) => (
627656
<li key={index} title={nls.localize('theia/ai/chat-ui/openDiff', 'Open Diff')} onClick={() => element.openChange?.()}>
628-
<div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`} />
629-
<span className='theia-ChatInput-ChangeSet-labelParts'>
657+
<div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`}>
658+
</div>
659+
<div className='theia-ChatInput-ChangeSet-labelParts'>
630660
<span className={`theia-ChatInput-ChangeSet-title ${element.nameClass}`}>
631661
{element.name}
632662
</span>
633-
<span className='theia-ChatInput-ChangeSet-additionalInfo'>
634-
{element.additionalInfo}
635-
</span>
636-
</span>
663+
<div className='theia-ChatInput-ChangeSet-additionalInfo'>
664+
{element.additionalInfo && <span>{element.additionalInfo}</span>}
665+
{element.additionalInfoSuffixIcon
666+
&& <div className={`theia-ChatInput-ChangeSet-AdditionalInfo-SuffixIcon ${element.additionalInfoSuffixIcon.join(' ')}`}></div>}
667+
</div>
668+
</div>
637669
<div className='theia-ChatInput-ChangeSet-Actions'>
638670
{element.open && (
639671
<span

packages/ai-chat-ui/src/browser/style/index.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ div:last-child > .theia-ChatNode {
336336
}
337337

338338
.theia-ChatInput-ChangeSet-List .theia-ChatInput-ChangeSet-Icon {
339+
position: relative;
339340
padding-left: 2px;
340341
padding-right: 4px;
341342
min-width: var(--theia-icon-size);
@@ -360,11 +361,20 @@ div:last-child > .theia-ChatNode {
360361
}
361362

362363
.theia-ChatInput-ChangeSet-additionalInfo {
364+
align-items: center;
365+
gap: 4px;
363366
margin-left: 8px;
364367
color: var(--theia-disabledForeground);
365368
}
366369

370+
.theia-ChatInput-ChangeSet-List
371+
.theia-ChatInput-ChangeSet-AdditionalInfo-SuffixIcon {
372+
font-size: var(--theia-ui-font-size0) px;
373+
margin-left: 4px;
374+
}
375+
367376
.theia-ChatInput-ChangeSet-labelParts {
377+
display: flex;
368378
overflow: hidden;
369379
text-overflow: ellipsis;
370380
}
@@ -389,6 +399,7 @@ div:last-child > .theia-ChatNode {
389399
.theia-ChatInput-ChangeSet-Header-Actions,
390400
.theia-ChatInput-ChangeSet-Box h3,
391401
.theia-ChatInput-ChangeSet-additionalInfo {
402+
display: flex;
392403
white-space: nowrap;
393404
overflow: hidden;
394405
text-overflow: ellipsis;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { ContextSummaryVariableContribution } from '../common/context-summary-va
4646
import { ContextDetailsVariableContribution } from '../common/context-details-variable';
4747
import { ChangeSetVariableContribution } from './change-set-variable';
4848
import { ChatSessionNamingAgent, ChatSessionNamingService } from '../common/chat-session-naming-service';
49+
import { ChangeSetDecorator, ChangeSetDecoratorService } from './change-set-decorator-service';
4950

5051
export default new ContainerModule(bind => {
5152
bindContributionProvider(bind, Agent);
@@ -106,6 +107,11 @@ export default new ContainerModule(bind => {
106107
container.bind(ChangeSetFileElement).toSelf().inSingletonScope();
107108
return container.get(ChangeSetFileElement);
108109
});
110+
111+
bind(ChangeSetDecoratorService).toSelf().inSingletonScope();
112+
bind(FrontendApplicationContribution).toService(ChangeSetDecoratorService);
113+
bindContributionProvider(bind, ChangeSetDecorator);
114+
109115
bind(ChangeSetFileResourceResolver).toSelf().inSingletonScope();
110116
bind(ResourceResolver).toService(ChangeSetFileResourceResolver);
111117
bind(ToolCallChatResponseContentFactory).toSelf().inSingletonScope();
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
17+
import { ContributionProvider, Emitter, type Event } from '@theia/core';
18+
import { type FrontendApplicationContribution } from '@theia/core/lib/browser';
19+
import { inject, injectable, named } from '@theia/core/shared/inversify';
20+
import debounce = require('@theia/core/shared/lodash.debounce');
21+
import type { ChangeSetDecoration, ChangeSetElement } from '../common';
22+
23+
/**
24+
* A decorator for a change set element.
25+
* It allows to add additional information to the element, such as icons.
26+
*/
27+
export const ChangeSetDecorator = Symbol('ChangeSetDecorator');
28+
export interface ChangeSetDecorator {
29+
readonly id: string;
30+
31+
readonly onDidChangeDecorations: Event<void>;
32+
33+
decorate(element: ChangeSetElement): ChangeSetDecoration | undefined;
34+
}
35+
36+
@injectable()
37+
export class ChangeSetDecoratorService implements FrontendApplicationContribution {
38+
39+
protected readonly onDidChangeDecorationsEmitter = new Emitter<void>();
40+
readonly onDidChangeDecorations = this.onDidChangeDecorationsEmitter.event;
41+
42+
@inject(ContributionProvider) @named(ChangeSetDecorator)
43+
protected readonly contributions: ContributionProvider<ChangeSetDecorator>;
44+
45+
initialize(): void {
46+
this.contributions.getContributions().map(decorator => decorator.onDidChangeDecorations(this.fireDidChangeDecorations));
47+
}
48+
49+
protected readonly fireDidChangeDecorations = debounce(() => {
50+
this.onDidChangeDecorationsEmitter.fire(undefined);
51+
}, 150);
52+
53+
getDecorations(element: ChangeSetElement): ChangeSetDecoration[] {
54+
const decorators = this.contributions.getContributions();
55+
const decorations: ChangeSetDecoration[] = [];
56+
for (const decorator of decorators) {
57+
const decoration = decorator.decorate(element);
58+
if (decoration) {
59+
decorations.push(decoration);
60+
}
61+
}
62+
63+
decorations.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
64+
65+
return decorations;
66+
}
67+
68+
getAdditionalInfoSuffixIcon(element: ChangeSetElement): string[] | undefined {
69+
const decorations = this.getDecorations(element);
70+
return decorations.find(d => d.additionalInfoSuffixIcon)?.additionalInfoSuffixIcon;
71+
}
72+
}

packages/ai-chat/src/browser/change-set-file-element.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { ChangeSetFileResourceResolver, createChangeSetFileUri, UpdatableReferen
2222
import { ChangeSetFileService } from './change-set-file-service';
2323
import { FileService } from '@theia/filesystem/lib/browser/file-service';
2424
import { ConfirmDialog } from '@theia/core/lib/browser';
25+
import { ChangeSetDecoratorService } from './change-set-decorator-service';
2526

2627
export const ChangeSetFileElementFactory = Symbol('ChangeSetFileElementFactory');
2728
export type ChangeSetFileElementFactory = (elementProps: ChangeSetElementArgs) => ChangeSetFileElement;
@@ -60,6 +61,9 @@ export class ChangeSetFileElement implements ChangeSetElement {
6061
@inject(ChangeSetFileService)
6162
protected readonly changeSetFileService: ChangeSetFileService;
6263

64+
@inject(ChangeSetDecoratorService)
65+
protected readonly changeSetDecoratorService: ChangeSetDecoratorService;
66+
6367
@inject(FileService)
6468
protected readonly fileService: FileService;
6569

@@ -73,6 +77,7 @@ export class ChangeSetFileElement implements ChangeSetElement {
7377

7478
protected readonly onDidChangeEmitter = new Emitter<void>();
7579
readonly onDidChange = this.onDidChangeEmitter.event;
80+
7681
protected readOnlyResource: UpdatableReferenceResource;
7782
protected changeResource: UpdatableReferenceResource;
7883

packages/ai-chat/src/common/chat-model.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ export interface ChangeSetElement {
228228
dispose?(): void;
229229
}
230230

231+
export interface ChangeSetDecoration {
232+
readonly priority?: number;
233+
readonly additionalInfoSuffixIcon?: string[];
234+
}
235+
231236
export interface ChatRequest {
232237
readonly text: string;
233238
readonly displayText?: string;

packages/ai-scanoss/src/browser/ai-scanoss-code-scan-action.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class ScanOSSScanButtonAction implements CodePartRendererAction {
5050
@inject(PreferenceService)
5151
protected readonly preferenceService: PreferenceService;
5252

53-
priority = 30;
53+
priority = 0;
5454

5555
canRender(response: CodeChatResponseContent, parentNode: ResponseNode): boolean {
5656
if (!hasScanOSSResults(parentNode.response.data)) {
@@ -144,9 +144,10 @@ const ScanOSSIntegration = React.memo((props: {
144144
title = nls.localize('theia/ai/scanoss/snippet/no-match', 'SCANOSS - No match');
145145
}
146146
}
147+
147148
return (
148149
<div
149-
className={`button scanoss-logo show-check icon-container ${scanOSSResult === 'pending'
150+
className={`button scanoss-icon icon-container ${scanOSSResult === 'pending'
150151
? 'pending'
151152
: scanOSSResult
152153
? scanOSSResult.type
@@ -156,7 +157,6 @@ const ScanOSSIntegration = React.memo((props: {
156157
role="button"
157158
onClick={scanOSSClicked}
158159
>
159-
<div className="codicon codicon-circle placeholder" />
160160
{scanOSSResult && scanOSSResult !== 'pending' && (
161161
<span className="status-icon">
162162
{scanOSSResult.type === 'clean' && <span className="codicon codicon-pass-filled" />}
@@ -192,8 +192,8 @@ export class ScanOSSDialog extends ReactDialog<void> {
192192
protected renderHeader(): React.ReactNode {
193193
return (
194194
<div className="scanoss-header">
195-
<div className="scanoss-logo-container">
196-
<div className="scanoss-logo"></div>
195+
<div className="scanoss-icon-container">
196+
<div className="scanoss-icon"></div>
197197
<h2>SCANOSS</h2>
198198
</div>
199199
</div>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@ import { ScanOSSScanButtonAction } from './ai-scanoss-code-scan-action';
2222
import { CodePartRendererAction } from '@theia/ai-chat-ui/lib/browser/chat-response-renderer';
2323
import { ChangeSetActionRenderer } from '@theia/ai-chat-ui/lib/browser/change-set-actions/change-set-action-service';
2424
import { ChangeSetScanActionRenderer } from './change-set-scan-action/change-set-scan-action';
25+
import { ChangeSetDecorator } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
26+
import { ChangeSetScanDecorator } from './change-set-scan-action/change-set-scan-decorator';
2527

2628
export default new ContainerModule(bind => {
2729
bind(PreferenceContribution).toConstantValue({ schema: AIScanOSSPreferencesSchema });
2830
bind(ScanOSSScanButtonAction).toSelf().inSingletonScope();
2931
bind(CodePartRendererAction).toService(ScanOSSScanButtonAction);
3032
bind(ChangeSetScanActionRenderer).toSelf();
3133
bind(ChangeSetActionRenderer).toService(ChangeSetScanActionRenderer);
34+
bind(ChangeSetScanDecorator).toSelf().inSingletonScope();
35+
bind(ChangeSetDecorator).toService(ChangeSetScanDecorator);
3236
});

0 commit comments

Comments
 (0)