Skip to content

Commit

Permalink
feat: change set support in chat input and chat model (#14750)
Browse files Browse the repository at this point in the history
* Add the concept of a change set to chat model and input UI
* Add an implementation of change set elements for files
* Add an agent for testing: `@ChangeSet`
* Integrates with `@Coder` agent

Other fixes in Chat Input UI:
* The inProgress state of the chat input was actually unsafely managed.
This change addresses the proper management of the inProgress state.
* The positioning, e.g. of the placeholder is now more adaptive.

As the change set feature directly relates to another feature (context,
work in progress), this change also already prepares for those changes
in the chat UI:
* Prepare chat input for adding context to requests
* Add context in the form of variables to chat model

Fixes #14749

Co-authored-by: Jonas Helming <[email protected]>
Co-authored-by: Stefan Dirix <[email protected]>
  • Loading branch information
3 people authored Jan 24, 2025
1 parent ecf65d3 commit 445b35b
Show file tree
Hide file tree
Showing 25 changed files with 1,235 additions and 806 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { bindTestSample } from './test/sample-test-contribution';
import { bindSampleFileSystemCapabilitiesCommands } from './file-system/sample-file-system-capabilities';
import { bindChatNodeToolbarActionContribution } from './chat/chat-node-toolbar-action-contribution';
import { bindAskAndContinueChatAgentContribution } from './chat/ask-and-continue-chat-agent-contribution';
import { bindChangeSetChatAgentContribution } from './chat/change-set-chat-agent-contribution';

export default new ContainerModule((
bind: interfaces.Bind,
Expand All @@ -40,6 +41,7 @@ export default new ContainerModule((
rebind: interfaces.Rebind,
) => {
bindAskAndContinueChatAgentContribution(bind);
bindChangeSetChatAgentContribution(bind);
bindChatNodeToolbarActionContribution(bind);
bindDynamicLabelProvider(bind);
bindSampleUnclosableView(bind);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// *****************************************************************************
// 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 {
AbstractStreamParsingChatAgent,
ChangeSetImpl,
ChatAgent,
ChatRequestModelImpl,
MarkdownChatResponseContentImpl,
SystemMessageDescription
} from '@theia/ai-chat';
import { ChangeSetFileElementFactory } from '@theia/ai-chat/lib/browser/change-set-file-element';
import { Agent, PromptTemplate } from '@theia/ai-core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';

export function bindChangeSetChatAgentContribution(bind: interfaces.Bind): void {
bind(ChangeSetChatAgent).toSelf().inSingletonScope();
bind(Agent).toService(ChangeSetChatAgent);
bind(ChatAgent).toService(ChangeSetChatAgent);
}

/**
* This is a test agent demonstrating how to create change sets in AI chats.
*/
@injectable()
export class ChangeSetChatAgent extends AbstractStreamParsingChatAgent implements ChatAgent {
override id = 'ChangeSet';
readonly name = 'ChangeSet';
override defaultLanguageModelPurpose = 'chat';
readonly description = 'This chat will create and modify a change set.';
readonly variables = [];
readonly agentSpecificVariables = [];
readonly functions = [];
override languageModelRequirements = [];
promptTemplates: PromptTemplate[] = [];

@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;

@inject(FileService)
protected readonly fileService: FileService;

@inject(ChangeSetFileElementFactory)
protected readonly fileChangeFactory: ChangeSetFileElementFactory;

override async invoke(request: ChatRequestModelImpl): Promise<void> {
const roots = this.workspaceService.tryGetRoots();
if (roots.length === 0) {
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'No workspace is open. For using this chat agent, please open a workspace with at least two files in the root.'
));
request.response.complete();
return;
}

const root = roots[0];
const files = root.children?.filter(child => child.isFile);
if (!files || files.length < 3) {
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'The workspace does not contain any files. For using this chat agent, please add at least two files in the root.'
));
request.response.complete();
return;
}

const fileToAdd = root.resource.resolve('hello/new-file.txt');
const fileToChange = files[Math.floor(Math.random() * files.length)];
const fileToDelete = files.filter(file => file.name !== fileToChange.name)[Math.floor(Math.random() * files.length)];

const chatSessionId = request.session.id;
const changeSet = new ChangeSetImpl('My Test Change Set');
changeSet.addElement(
this.fileChangeFactory({
uri: fileToAdd,
type: 'add',
state: 'pending',
targetState: 'Hello World!',
changeSet,
chatSessionId
})
);
if (fileToChange && fileToChange.resource) {
changeSet.addElement(
this.fileChangeFactory({
uri: fileToChange.resource,
type: 'modify',
state: 'pending',
targetState: 'Hello World Modify!',
changeSet,
chatSessionId
})
);
}
if (fileToDelete && fileToDelete.resource) {
changeSet.addElement(
this.fileChangeFactory({
uri: fileToDelete.resource,
type: 'delete',
state: 'pending',
changeSet,
chatSessionId
})
);
}
request.session.setChangeSet(changeSet);

request.response.response.addContent(new MarkdownChatResponseContentImpl(
'I have created a change set for you. You can now review and apply it.'
));
request.response.complete();
}

protected override async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {
return undefined;
}
}

1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { EditorManager } from '@theia/editor/lib/browser';
import { AIChatContribution } from './ai-chat-ui-contribution';
import { AIChatInputWidget } from './chat-input-widget';
import { AIChatInputConfiguration, AIChatInputWidget } from './chat-input-widget';
import { ChatNodeToolbarActionContribution } from './chat-node-toolbar-action-contribution';
import { ChatResponsePartRenderer } from './chat-response-part-renderer';
import {
Expand Down Expand Up @@ -60,6 +60,9 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
bindChatViewWidget(bind);

bind(AIChatInputWidget).toSelf();
bind(AIChatInputConfiguration).toConstantValue({
showContext: false
});
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: AIChatInputWidget.ID,
createWidget: () => container.get(AIChatInputWidget)
Expand Down
Loading

0 comments on commit 445b35b

Please sign in to comment.