Skip to content

Commit 1e2270a

Browse files
committed
add session settings
1 parent 7c97481 commit 1e2270a

File tree

8 files changed

+247
-3
lines changed

8 files changed

+247
-3
lines changed

packages/ai-chat-ui/src/browser/chat-view-commands.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export namespace ChatCommands {
3232
category: CHAT_CATEGORY,
3333
iconClass: codicon('lock')
3434
}, '', CHAT_CATEGORY_KEY);
35+
36+
export const EDIT_SESSION_SETTINGS = Command.toLocalizedCommand({
37+
id: 'chat:widget:session-settings',
38+
category: CHAT_CATEGORY,
39+
iconClass: codicon('bracket')
40+
}, 'Set Session Settings', CHAT_CATEGORY_KEY);
3541
}
3642

3743
export const AI_CHAT_NEW_CHAT_WINDOW_COMMAND: Command = {

packages/ai-chat-ui/src/browser/chat-view-language-contribution.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { ProviderResult } from '@theia/monaco-editor-core/esm/vs/editor/common/l
2525
import { ChatViewWidget } from './chat-view-widget';
2626

2727
export const CHAT_VIEW_LANGUAGE_ID = 'theia-ai-chat-view-language';
28+
export const SETTINGS_LANGUAGE_ID = 'theia-ai-chat-settings-language';
2829
export const CHAT_VIEW_LANGUAGE_EXTENSION = 'aichatviewlanguage';
2930

3031
const VARIABLE_RESOLUTION_CONTEXT = { context: 'chat-input-autocomplete' };
@@ -58,6 +59,7 @@ export class ChatViewLanguageContribution implements FrontendApplicationContribu
5859

5960
onStart(_app: FrontendApplication): MaybePromise<void> {
6061
monaco.languages.register({ id: CHAT_VIEW_LANGUAGE_ID, extensions: [CHAT_VIEW_LANGUAGE_EXTENSION] });
62+
monaco.languages.register({ id: SETTINGS_LANGUAGE_ID, extensions: ['json'], filenames: ['editor'] });
6163

6264
this.registerCompletionProviders();
6365

packages/ai-chat-ui/src/browser/chat-view-widget-toolbar-contribution.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,45 @@
1717
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
1818
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
1919
import { AIChatContribution } from './ai-chat-ui-contribution';
20-
import { Emitter, nls } from '@theia/core';
20+
import { Emitter, InMemoryResources, URI, nls } from '@theia/core';
2121
import { ChatCommands } from './chat-view-commands';
22+
import { CommandRegistry } from '@theia/core/lib/common/command';
23+
import { SessionSettingsDialog } from './session-settings-dialog';
24+
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
25+
import { ChatViewWidget } from './chat-view-widget';
2226

2327
@injectable()
2428
export class ChatViewWidgetToolbarContribution implements TabBarToolbarContribution {
2529
@inject(AIChatContribution)
2630
protected readonly chatContribution: AIChatContribution;
2731

32+
@inject(CommandRegistry)
33+
protected readonly commandRegistry: CommandRegistry;
34+
35+
@inject(MonacoEditorProvider)
36+
protected readonly editorProvider: MonacoEditorProvider;
37+
38+
@inject(InMemoryResources)
39+
protected readonly resources: InMemoryResources;
40+
2841
protected readonly onChatWidgetStateChangedEmitter = new Emitter<void>();
2942
protected readonly onChatWidgetStateChanged = this.onChatWidgetStateChangedEmitter.event;
3043

44+
private readonly sessionSettingsURI = new URI('json-data-dialog:/editor.json');
45+
3146
@postConstruct()
3247
protected init(): void {
48+
this.resources.add(this.sessionSettingsURI, '');
49+
3350
this.chatContribution.widget.then(widget => {
3451
widget.onStateChanged(() => this.onChatWidgetStateChangedEmitter.fire());
3552
});
53+
54+
this.commandRegistry.registerCommand(ChatCommands.EDIT_SESSION_SETTINGS, {
55+
execute: () => this.openJsonDataDialog(),
56+
isEnabled: widget => widget instanceof ChatViewWidget,
57+
isVisible: widget => widget instanceof ChatViewWidget
58+
});
3659
}
3760

3861
registerToolbarItems(registry: TabBarToolbarRegistry): void {
@@ -50,5 +73,28 @@ export class ChatViewWidgetToolbarContribution implements TabBarToolbarContribut
5073
onDidChange: this.onChatWidgetStateChanged,
5174
priority: 2
5275
});
76+
registry.registerItem({
77+
id: ChatCommands.EDIT_SESSION_SETTINGS.id,
78+
command: ChatCommands.EDIT_SESSION_SETTINGS.id,
79+
tooltip: nls.localize('theia/ai/session-settings-dialog/tooltip', 'Set Session Settings'),
80+
priority: 3
81+
});
82+
}
83+
84+
protected async openJsonDataDialog(): Promise<void> {
85+
const widget = await this.chatContribution.widget;
86+
if (!widget) {
87+
return;
88+
}
89+
90+
const dialog = new SessionSettingsDialog(this.editorProvider, this.resources, this.sessionSettingsURI, {
91+
initialSettings: widget.getSettings()
92+
});
93+
94+
const result = await dialog.open();
95+
if (result) {
96+
widget.setSettings(result);
97+
}
98+
5399
}
54100
}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616
import { CommandService, deepClone, Emitter, Event, MessageService } from '@theia/core';
17-
import { ChatRequest, ChatRequestModel, ChatService, ChatSession, isActiveSessionChangedEvent } from '@theia/ai-chat';
17+
import { ChatRequest, ChatRequestModel, ChatService, ChatSession, isActiveSessionChangedEvent, MutableChatModel } from '@theia/ai-chat';
1818
import { BaseWidget, codicon, ExtractableWidget, Message, PanelLayout, PreferenceService, StatefulWidget } from '@theia/core/lib/browser';
1919
import { nls } from '@theia/core/lib/common/nls';
2020
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
@@ -220,4 +220,15 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
220220
addContext(variable: AIVariableResolutionRequest): void {
221221
this.inputWidget.addContext(variable);
222222
}
223+
224+
setSettings(settings: { [key: string]: unknown }): void {
225+
if (this.chatSession && this.chatSession.model) {
226+
const model = this.chatSession.model as MutableChatModel;
227+
model.setSettings(settings);
228+
}
229+
}
230+
231+
getSettings(): { [key: string]: unknown } | undefined {
232+
return this.chatSession.model.settings;
233+
}
223234
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 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 { InMemoryResources, URI, nls } from '@theia/core';
18+
import { AbstractDialog } from '@theia/core/lib/browser/dialogs';
19+
20+
import { Message } from '@theia/core/lib/browser';
21+
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
22+
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
23+
24+
export interface SessionSettingsDialogProps {
25+
initialSettings: { [key: string]: unknown } | undefined;
26+
}
27+
28+
const DEFAULT_DIALOG_HEIGHT = 400;
29+
const DEFAULT_DIALOG_WIDTH = 600;
30+
31+
export class SessionSettingsDialog extends AbstractDialog<{ [key: string]: unknown }> {
32+
33+
protected jsonEditor: MonacoEditor | undefined;
34+
protected dialogContent: HTMLDivElement;
35+
protected errorMessageDiv: HTMLDivElement;
36+
protected settings: { [key: string]: unknown } = {};
37+
protected initialSettingsString: string;
38+
39+
constructor(
40+
protected readonly editorProvider: MonacoEditorProvider,
41+
protected readonly resources: InMemoryResources,
42+
protected readonly uri: URI,
43+
protected readonly options: SessionSettingsDialogProps
44+
) {
45+
super({
46+
title: nls.localize('theia/ai/session-settings-dialog/title', 'Set Session Settings')
47+
});
48+
49+
const initialSettings = options.initialSettings;
50+
this.initialSettingsString = JSON.stringify(initialSettings, undefined, 2) || '{}';
51+
52+
this.contentNode.classList.add('monaco-session-settings-dialog');
53+
this.contentNode.style.width = `${DEFAULT_DIALOG_WIDTH}px`;
54+
this.contentNode.style.height = `${DEFAULT_DIALOG_HEIGHT}px`;
55+
56+
this.dialogContent = document.createElement('div');
57+
this.dialogContent.className = 'session-settings-container';
58+
this.dialogContent.style.height = `${DEFAULT_DIALOG_HEIGHT - 70}px`;
59+
this.contentNode.appendChild(this.dialogContent);
60+
61+
this.errorMessageDiv = document.createElement('div');
62+
this.errorMessageDiv.className = 'session-settings-error';
63+
this.contentNode.appendChild(this.errorMessageDiv);
64+
65+
this.appendCloseButton(nls.localizeByDefault('Cancel'));
66+
this.appendAcceptButton(nls.localizeByDefault('Apply'));
67+
68+
this.createJsonEditor();
69+
70+
this.validateJson();
71+
}
72+
73+
protected override onAfterAttach(msg: Message): void {
74+
super.onAfterAttach(msg);
75+
this.update();
76+
}
77+
78+
protected override onActivateRequest(msg: Message): void {
79+
super.onActivateRequest(msg);
80+
if (this.jsonEditor) {
81+
this.jsonEditor.focus();
82+
}
83+
}
84+
protected async createJsonEditor(): Promise<void> {
85+
86+
this.resources.update(this.uri, this.initialSettingsString);
87+
try {
88+
const editor = await this.editorProvider.createInline(this.uri, this.dialogContent, {
89+
language: 'json',
90+
automaticLayout: true,
91+
minimap: {
92+
enabled: false
93+
},
94+
scrollBeyondLastLine: false,
95+
folding: true,
96+
lineNumbers: 'on',
97+
fontSize: 13,
98+
wordWrap: 'on',
99+
renderValidationDecorations: 'on',
100+
scrollbar: {
101+
vertical: 'auto',
102+
horizontal: 'auto'
103+
}
104+
});
105+
106+
editor.getControl().onDidChangeModelContent(() => {
107+
this.validateJson();
108+
});
109+
editor.document.textEditorModel.setValue(this.initialSettingsString);
110+
111+
this.jsonEditor = editor;
112+
this.validateJson();
113+
} catch (error) {
114+
console.error('Failed to create JSON editor:', error);
115+
}
116+
}
117+
118+
protected validateJson(): void {
119+
if (!this.jsonEditor) {
120+
return;
121+
}
122+
123+
const jsonContent = this.jsonEditor.getControl().getValue();
124+
125+
try {
126+
this.settings = JSON.parse(jsonContent);
127+
this.errorMessageDiv.textContent = '';
128+
this.errorMessageDiv.style.display = 'none';
129+
this.setErrorButtonState(false);
130+
} catch (error) {
131+
this.errorMessageDiv.textContent = `${error}`;
132+
this.errorMessageDiv.style.display = 'block';
133+
this.setErrorButtonState(true);
134+
}
135+
}
136+
137+
protected setErrorButtonState(isError: boolean): void {
138+
const acceptButton = this.acceptButton;
139+
if (acceptButton) {
140+
acceptButton.disabled = isError;
141+
if (isError) {
142+
acceptButton.classList.add('disabled');
143+
} else {
144+
acceptButton.classList.remove('disabled');
145+
}
146+
}
147+
}
148+
149+
get value(): { [key: string]: unknown } {
150+
return this.settings;
151+
}
152+
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,3 +679,19 @@ details[open].collapsible-arguments .collapsible-arguments-summary {
679679
.section-content strong {
680680
font-weight: bold;
681681
}
682+
683+
.session-settings-container {
684+
margin-bottom: 10px;
685+
}
686+
687+
.json-editor-description {
688+
margin-bottom: 10px;
689+
color: var(--theia-foreground);
690+
}
691+
692+
.monaco-session-settings-dialog {
693+
flex: 1;
694+
min-height: 350px;
695+
border: 1px solid var(--theia-editorWidget-border);
696+
margin-bottom: 10px;
697+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,8 @@ export abstract class AbstractChatAgent implements ChatAgent {
280280
toolRequests: ChatToolRequest[],
281281
languageModel: LanguageModel
282282
): Promise<LanguageModelResponse> {
283-
const settings = this.getLlmSettings();
283+
const agentSettings = this.getLlmSettings();
284+
const settings = { ...agentSettings, ...request.session.settings };
284285
const tools = toolRequests.length > 0 ? toolRequests : undefined;
285286
return this.languageModelService.sendRequest(
286287
languageModel,

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export interface ChatModel {
9595
readonly location: ChatAgentLocation;
9696
readonly changeSet?: ChangeSet;
9797
readonly context: ChatContextManager;
98+
readonly settings?: { [key: string]: unknown };
9899
getRequests(): ChatRequestModel[];
99100
isEmpty(): boolean;
100101
}
@@ -522,6 +523,7 @@ export class MutableChatModel implements ChatModel, Disposable {
522523
protected _id: string;
523524
protected _changeSet?: ChangeSetImpl;
524525
protected readonly _contextManager = new ChatContextManagerImpl();
526+
protected _settings: { [key: string]: unknown };
525527

526528
constructor(public readonly location = ChatAgentLocation.Panel) {
527529
// TODO accept serialized data as a parameter to restore a previously saved ChatModel
@@ -550,6 +552,14 @@ export class MutableChatModel implements ChatModel, Disposable {
550552
return this._contextManager;
551553
}
552554

555+
get settings(): { [key: string]: unknown } {
556+
return this._settings;
557+
}
558+
559+
setSettings(settings: { [key: string]: unknown }): void {
560+
this._settings = settings;
561+
}
562+
553563
setChangeSet(changeSet: ChangeSetImpl | undefined): void {
554564
if (!changeSet) {
555565
return this.removeChangeSet();

0 commit comments

Comments
 (0)