Compare commits

...

3 commits

Author SHA1 Message Date
igardev
ff3dfe6142 Edit Agent view added 2025-10-14 14:45:16 +03:00
igardev
87cd6bbdfd - Added menu elements for adding a model from OpenAI compatible provider (like OpenRouter)
- Added buttons in Current Environment view for showing the other possible actions for the model type
2025-10-12 20:22:52 +03:00
igardev
42b500add2 Remove the todo from the agent view 2025-10-08 17:43:19 +03:00
20 changed files with 1125 additions and 93 deletions

View file

@ -124,6 +124,38 @@ You could delete the GGUF files from this folder. If they are missing, but are n
## Edit Agent
### Overview
Edit agent view is used for adding and editing agents. From there it is also possible to delete and copy an existing agent as a new one. The identifier of an agent is it's name. For now there is no tools model as part of the agent (the currently selected tools model will be used)
### How to use it
Edit existing agent:
1. Click Select button and load an agent to be edited.
2. Change the Description and System Instructions fields (if needed)
3. Click Add Tools button and select the tools to be used for by the agent.
4. Click Save button
Add new agent:
1. Click New button
2. Enter Name, Description and System Instructions for the agent
3. Click Add Tools button and select the tools to be used for by the agent.
4. Click Save button
Copy existing agent as a new one:
1. Click Copy as New button
2. Edit Name, Description and System Instructions for the agent
3. Click Add Tools button and select the tools to be used for by the agent.
4. Click Save button
Delete agent:
1. Click Delete button
3. Select an agent to be deleted from the list
4. Confirm the deletion of the agent
## Edit with AI
### Requred servers
@ -167,12 +199,12 @@ This generate a commit message, based on the current changes.
![Generate a commit message](https://github.com/user-attachments/assets/25f5d1ae-3673-4416-ba52-7615969c1bb3)
## Version 0.0.27 is released (21.09.2025)
## Version 0.0.32 is released (05.10.2025)
## What is new
- xAI Grog4 free (from OpenRouter) added to the initial models
- Chat with AI with project context removed (agent does it better)
- Chat with AI about llama-vscode is now with agent, not using webui
- Agent - new buttons "Tools Model" and "Agent" - possibility to view the selected model and agent and to change them.
- predefined model DeepSeek V3.1 free 163,800 context (OpenRouter) added
- predefined model Z.AI: GLM 4.5 Air (free): GLM 4.5 Air - 128.000 context (OpenRouter) added
- Added agent "Ask" is for review, analysis and suggestions for the code without changing the files
- Some bugs are fixed
## Setup instructions for llama.cpp server
@ -320,7 +352,7 @@ https://github.com/user-attachments/assets/e75e96de-878b-43db-a45b-47cc0c554697
### Overview
Agent is combination of system prompt and tools. If an agent is selected, it will be used by the Llama Agent UI. On slecting and agent, the selected llama-vscode tools are updated.
They have properties: name, description, syste prompt, tools.
They have properties: name, description, system prompt, tools.
Agent could be added/deleted/viewed/selected/deselected/exported/imported
@ -330,6 +362,12 @@ Select "Agents..." from llama-vscode menu
- Add agent...
Adds an agent
- Edit agent...
Edits an agent
- Copy agent...
Copies an agent
- Delete agent...
Deletes an agent
@ -390,6 +428,9 @@ Deselect the currently selected model. If the model is local, the llama.cpp serv
- Add model from huggingface
Enter search words to find a model from huggingface. If the model is selected it will be automatically downloaded (if not yet done) and a llama.cpp server will be started with it.
- Add chat model from OpenAI compatible provider
Add chat model from OpenAI compatible provider - OpenRouter or custom (for example local/external llama.cpp server).
- Export
A model could be exported as a .json files. This file could be shared with other users, modified if needed and imported again. Select a model to export it.
@ -455,7 +496,7 @@ Select the model you want to delete from the list and delete it.
- View
Select a model from the list to view all the details for this model
- Selected
- Select
Select a model from the list to select it. If the model is a local one (has a command in local start command) a llama.cpp server with this model will be started. Only one completion model could be selected at a time.
- Deselect
@ -464,6 +505,9 @@ Deselect the currently selected model. If the model is local, the llama.cpp serv
- Add model from huggingface
Enter search words to find a model from huggingface. If the model is selected it will be automatically downloaded (if not yet done) and a llama.cpp server will be started with it.
- Add completion model from OpenAI compatible provider
Add completion model from OpenAI compatible provider - OpenRouter or custom (for example local/external llama.cpp server).
- Export
A model could be exported as a .json files. This file could be shared with other users, modified if needed and imported again. Select a model to export it.
@ -511,6 +555,9 @@ Deselect the currently selected model. If the model is local, the llama.cpp serv
- Add model from huggingface
Enter search words to find a model from huggingface. If the model is selected it will be automatically downloaded (if not yet done) and a llama.cpp server will be started with it.
- Add embeddings model from OpenAI compatible provider
Add embeddings model from OpenAI compatible provider - OpenRouter or custom (for example local/external llama.cpp server).
- Export
A model could be exported as a .json files. This file could be shared with others used, modified if needed and imported again. Select a model to export it.
@ -608,6 +655,9 @@ Deselect the currently selected model. If the model is local, the llama.cpp serv
- Add model from huggingface
Enter search words to find a model from huggingface. If the model is selected it will be automatically downloaded (if not yet done) and a llama.cpp server will be started with it.
- Add tools model from OpenAI compatible provider
Add tools model from OpenAI compatible provider - OpenRouter or custom (for example local/external llama.cpp server).
- Export
A model could be exported as a .json files. This file could be shared with other users, modified if needed and imported again. Select a model to export it.

View file

@ -28,6 +28,7 @@ import { ChatService } from "./services/chat-service";
import { Agent, Chat, Env, LlmModel } from "./types";
import { ModelType, PERSISTENCE_KEYS } from "./constants";
import { ApiKeyService } from "./services/api-key-service";
import { OpenAiCompModelStrategy } from "./services/openai-comp-model-strategy";
export class Application {
@ -54,6 +55,7 @@ export class Application {
public hfModelStrategy: HfModelStrategy
public localModelStrategy: LocalModelStrategy
public externalModelStrategy: ExternalModelStrategy
public openAiCompModelStrategy: OpenAiCompModelStrategy
public envService: EnvService
public agentService: AgentService
public agentCommandService: AgentCommandService
@ -91,6 +93,7 @@ export class Application {
this.hfModelStrategy = new HfModelStrategy(this)
this.localModelStrategy = new LocalModelStrategy(this)
this.externalModelStrategy = new ExternalModelStrategy(this)
this.openAiCompModelStrategy = new OpenAiCompModelStrategy(this)
this.modelService = new ModelService(this)
this.envService = new EnvService(this)
this.agentService = new AgentService(this)

View file

@ -454,7 +454,7 @@ export class Architect {
if (result.includes("command not found") || result.includes("is not recognized")) {
let questionInstall = "llama.cpp will be installed as it is requred by llama-vscode extension.";
if (process.platform == 'win32') questionInstall += "\nVS Code will be restarted.";
let shouldInstall = await Utils.showUserChoiceDialog(questionInstall, "Confirm");
let [shouldInstall, shouldStopAsking] = await Utils.showYesNoNodontAskDialog(questionInstall, "Confirm");
if (shouldInstall) {
await this.app.menu.installLlamacpp();
this.app.persistence.setGlobalValue("last_llama_cpp", (new Date()).toISOString());
@ -464,8 +464,6 @@ export class Architect {
}, 2000);
}
} else {
let questionStopAskingLlamaCppInstall = "Do you prefer to stop getting a suggestion to install llama.cpp?"
let shouldStopAsking = await Utils.showUserChoiceDialog(questionStopAskingLlamaCppInstall, "Yes");
if (shouldStopAsking) this.app.configuration.updateConfigValue("ask_install_llamacpp", false);
}
} else {
@ -473,7 +471,7 @@ export class Architect {
let lastUpgradeDateStr = this.app.persistence.getGlobalValue("last_llama_cpp");
if (!lastUpgradeDateStr || Utils.isTimeToUpgrade(new Date(lastUpgradeDateStr), new Date(), this.app.configuration.ask_upgrade_llamacpp_hours)) {
let questionInstall = "Do you want to upgrade llama.cpp (used for running local models)? (recommended).";
let shouldInstall = await Utils.showUserChoiceDialog(questionInstall, "Confirm"); //yes, don't ask again
let [shouldInstall, shouldStopAsking] = await Utils.showYesNoNodontAskDialog(questionInstall, "Confirm");
if (shouldInstall) {
await this.app.menu.installLlamacpp();
this.app.persistence.setGlobalValue("last_llama_cpp", (new Date()).toISOString());
@ -492,8 +490,6 @@ export class Architect {
this.app.configuration.updateConfigValue(SETTING_NAME_FOR_LIST.ENVS, envs);
}
} else {
let questionStopAskingLlamaCppUpgrade = "Do you prefer to stop getting a suggestion to upgrade llama.cpp?"
let shouldStopAsking = await Utils.showUserChoiceDialog(questionStopAskingLlamaCppUpgrade, "Yes");
if (shouldStopAsking){
if (!lastUpgradeDateStr) this.app.persistence.setGlobalValue("last_llama_cpp", (new Date()).toISOString());
this.app.configuration.updateConfigValue("ask_upgrade_llamacpp_hours", 72000); // more than 8 years

View file

@ -175,6 +175,8 @@ export const UI_TEXT_KEYS = {
selectStartAgent: "Select/start agent...",
deselectStopAgent: "Deselect/stop agent...",
addAgent: "Add agent...",
editAgent: "Edit agent...",
copyAgent: "Copy agent...",
viewAgentDetails: "View agent details...",
deleteAgent: "Delete agent...",
exportAgent: "Export agent...",
@ -199,6 +201,7 @@ export const UI_TEXT_KEYS = {
addLocalCompletionModel: "Add local completion model...",
addExternalCompletionModel: "Add external completion model...",
addCompletionModelFromHuggingface: "Add completion model from huggingface...",
addCompletionOpenAiCompModel: "Add completion model from OpenAI compatible provider...",
viewCompletionModelDetails: "View completion model details...",
deleteCompletionModel: "Delete completion model...",
exportCompletionModel: "Export completion model...",
@ -210,6 +213,7 @@ export const UI_TEXT_KEYS = {
addLocalChatModel: "Add local chat model...",
addExternalChatModel: "Add external chat model...",
addChatModelFromHuggingface: "Add chat model from huggingface...",
addChatOpenAiCompModel: "Add chat model from OpenAI compatible provider...",
viewChatModelDetails: "View chat model details...",
deleteChatModel: "Delete chat model...",
exportChatModel: "Export chat model...",
@ -221,6 +225,7 @@ export const UI_TEXT_KEYS = {
addLocalEmbeddingsModel: "Add local embeddings model...",
addExternalEmbeddingsModel: "Add external embeddings model...",
addEmbeddingsModelFromHuggingface: "Add embeddings model from huggingface...",
addEmbeddingsOpenAiCompModel: "Add embeddings model from OpenAI compatible provider...",
viewEmbeddingsModelDetails: "View embeddings model details...",
deleteEmbeddingsModel: "Delete embeddings model...",
exportEmbeddingsModel: "Export embeddings model...",
@ -232,6 +237,7 @@ export const UI_TEXT_KEYS = {
addLocalToolsModel: "Add local tools model...",
addExternalToolsModel: "Add external tools model...",
addToolsModelFromHuggingface: "Add tools model from huggingface...",
addToolsOpenAiCompModel: "Add tools model from OpenAI compatible provider...",
viewToolsModelDetails: "View tools model details...",
deleteToolsModel: "Delete tools model...",
exportToolsModel: "Export tools model...",
@ -262,4 +268,13 @@ export const PREDEFINED_LISTS_KEYS = {
ENVS: SETTING_NAME_FOR_LIST.ENVS,
AGENTS: SETTING_NAME_FOR_LIST.AGENTS,
AGENT_COMMANDS: SETTING_NAME_FOR_LIST.AGENT_COMMANDS,
} as const;
} as const;
export enum OpenAiProvidersKeys {
OpenRouter = 'OpenRouter...',
Custom = 'Custom...'
}
export const OPENAI_COMP_PROVIDERS = {
[OpenAiProvidersKeys.OpenRouter]: "https://openrouter.ai/api",
[OpenAiProvidersKeys.Custom]: ""
} as const

View file

@ -629,7 +629,7 @@ export const PREDEFINED_LISTS = new Map<string, any>([
},
{
"name": "default",
"description": "This is the default agent.",
"description": "Agent for agentic programming - could answer questions, change/add/delete file, execute terminal commands, etc.",
"systemInstruction": [
"You are an agent for software development - please keep going until the users query is completely resolved, before ending your turn and yielding back to the user.",
"Only terminate your turn when you are sure that the problem is solved.",

View file

@ -241,7 +241,7 @@ export class LlamaServer {
"temperature": 0.8,
"top_p": 0.95,
...(model.trim() != "" && { model: model}),
"tools": [...this.app.tools.tools, ...this.app.tools.vscodeTools],
"tools": [...this.app.tools.getTools(), ...this.app.tools.vscodeTools],
"tool_choice": "auto"
};
}

View file

@ -66,6 +66,16 @@ export class LlamaWebviewProvider implements vscode.WebviewViewProvider {
case 'configureTools':
await this.app.tools.selectTools()
break;
case 'configureEditTools':
const selectedTools = await this.app.agentService.selectTools(message.tools)
this.app.agentService.resetEditedAgentTools();
selectedTools.map(toolName => this.app.agentService.addEditedAgentTools(toolName,""))
let selAgentTools = this.app.agentService.getEditedAgentTools();
webviewView.webview.postMessage({
command: 'updateAgentTools',
files: Array.from(selAgentTools.entries())
});
break;
case 'stopSession':
this.app.llamaAgent.stopAgent();
break;
@ -84,6 +94,21 @@ export class LlamaWebviewProvider implements vscode.WebviewViewProvider {
case 'deselectCompletionModel':
await this.app.modelService.deselectAndClearModel(ModelType.Completion);
break;
case 'moreCompletionModel':
await this.app.modelService.processModelActions(ModelType.Completion);
break;
case 'moreChatModel':
await this.app.modelService.processModelActions(ModelType.Chat);
break;
case 'moreEmbeddingsModel':
await this.app.modelService.processModelActions(ModelType.Embeddings);
break;
case 'moreToolsModel':
await this.app.modelService.processModelActions(ModelType.Tools);
break;
case 'moreAgent':
await this.app.agentService.processActions();
break;
case 'deselectChatModel':
await this.app.modelService.deselectAndClearModel(ModelType.Chat);
break;
@ -96,6 +121,9 @@ export class LlamaWebviewProvider implements vscode.WebviewViewProvider {
case 'deselectAgent':
await this.app.agentService.deselectAgent();
break;
case 'selectEditAgent':
await this.app.agentService.editAgent(this.app.configuration.agents_list)
break;
case 'showCompletionModel':
this.app.modelService.showModelDetails(this.app.getComplModel());
break;
@ -137,6 +165,9 @@ export class LlamaWebviewProvider implements vscode.WebviewViewProvider {
case 'showAgentView':
this.showAgentView();
break;
case 'showAgentEditor':
this.showAgentEditor();
break;
case 'showSelectedModels':
await this.app.envService.showCurrentEnv();
break;
@ -163,6 +194,13 @@ export class LlamaWebviewProvider implements vscode.WebviewViewProvider {
files: agentCommands
});
break;
case 'getAgentTools':
let agentTools = this.app.tools.getTools().map(tool => tool.function.name + " | " + tool.function.description)
webviewView.webview.postMessage({
command: 'updateFileList',
files: agentTools
});
break;
case 'addContextProjectFile':
let fileNames = message.fileLongName.split("|");
this.app.llamaAgent.addContextProjectFile(fileNames[1].trim(),fileNames[0].trim());
@ -180,6 +218,62 @@ export class LlamaWebviewProvider implements vscode.WebviewViewProvider {
files: Array.from(updatedContextFiles.entries())
});
break;
case 'addEditedAgentTool':
let toolsNames = message.fileLongName.split("|");
this.app.agentService.addEditedAgentTools(toolsNames[0].trim(),toolsNames[1].trim());
const editedAgentTools = this.app.agentService.getEditedAgentTools();
webviewView.webview.postMessage({
command: 'updateAgentTools',
files: Array.from(editedAgentTools.entries())
});
break;
case 'removeEditedAgentTool':
this.app.agentService.removeEditedAgentTools(message.fileLongName);
const updatedTools = this.app.agentService.getEditedAgentTools();
webviewView.webview.postMessage({
command: 'updateAgentTools',
files: Array.from(updatedTools.entries())
});
break;
case 'saveEditAgent':
if (!message.name) {
vscode.window.showErrorMessage("Agent should have a name!")
return;
}
let agentToSave: Agent = {
name: message.name,
description: message.description,
systemInstruction: message.systemInstruction.split(/\r?\n/),
tools: message.tools
}
await this.app.agentService.addUpdateAgent(agentToSave)
break;
case 'refreshEditedAgentTool':
const refreshedTols = this.app.agentService.getEditedAgentTools();
webviewView.webview.postMessage({
command: 'updateAgentTools',
files: Array.from(refreshedTols.entries())
});
break;
case 'editSelectedAgent':
const selectedAgent = this.app.getAgent()
this.addEditAgent(selectedAgent);
break
case 'addEditAgent':
const newAgent: Agent = {
name: message.name,
description: message.description,
systemInstruction: message.systemInstruction,
tools: message.tools
}
this.addEditAgent(newAgent);
break
case 'copyAsNewAgent':
this.app.agentService.copyAgent()
break;
case 'deleteAgent':
this.app.agentService.deleteAgent()
break;
case 'openContextFile':
const uri = vscode.Uri.file(message.fileLongName);
const document = await vscode.workspace.openTextDocument(uri);
@ -223,8 +317,21 @@ export class LlamaWebviewProvider implements vscode.WebviewViewProvider {
}, 1000);
}
public addEditAgent(agent: Agent) {
this.app.agentService.resetEditedAgentTools();
agent.tools?.map(tool => this.app.agentService.addEditedAgentTools(tool, ""));
const edAgtools = this.app.agentService.getEditedAgentTools();
vscode.commands.executeCommand('llama-vscode.webview.postMessage', {
command: 'loadAgent',
name: agent?.name,
description: agent?.description,
systemInstruction: agent?.systemInstruction.join("\n"),
tools: Array.from(edAgtools.entries())
});
}
private updateSettingInEnvView(key: string, settingValue: any) {
vscode.commands.executeCommand('llama-vscode.webview.postMessage', {
vscode.commands.executeCommand('llama-vscode.webview.postMessage', {
command: 'vscodeSettingValue',
key: key,
value: settingValue
@ -333,6 +440,11 @@ export class LlamaWebviewProvider implements vscode.WebviewViewProvider {
setTimeout(() => this.setView("addenv"), 500);
}
public showAgentEditor() {
vscode.commands.executeCommand('extension.showLlamaWebview');
setTimeout(() => this.setView("agenteditor"), 400);
}
public updateLlamaView() {
this.updateToolsModel();
this.updateChatModel();

View file

@ -246,9 +246,7 @@ export class Menu {
if (envSelected) await this.app.envService.processActions(envSelected);
break;
case this.app.configuration.getUiText(UI_TEXT_KEYS.agents) ?? "":
let agentsActions: vscode.QuickPickItem[] = this.app.agentService.getActions();
let actionSelected = await vscode.window.showQuickPick(agentsActions);
if (actionSelected) await this.app.agentService.processActions(actionSelected);
await this.app.agentService.processActions();
break;
case this.app.configuration.getUiText(UI_TEXT_KEYS.agentCommands) ?? "":
let agentCommandsActions: vscode.QuickPickItem[] = this.app.agentCommandService.getAgentCommandsActions();

View file

@ -1,4 +1,3 @@
import * as vscode from "vscode";
import { QuickPickItem } from "vscode";
import { Application } from "../application";
@ -11,6 +10,7 @@ import { UI_TEXT_KEYS, PERSISTENCE_KEYS, SETTING_NAME_FOR_LIST, PREDEFINED_LISTS
export class AgentService {
private app: Application;
public editedAgentTools: Map<string,string> = new Map();
constructor(app: Application) {
this.app = app;
@ -27,6 +27,12 @@ export class AgentService {
{
label: this.app.configuration.getUiText(UI_TEXT_KEYS.addAgent) ?? "",
},
{
label: this.app.configuration.getUiText(UI_TEXT_KEYS.editAgent) ?? "",
},
{
label: this.app.configuration.getUiText(UI_TEXT_KEYS.copyAgent) ?? "",
},
{
label: this.app.configuration.getUiText(UI_TEXT_KEYS.viewAgentDetails) ?? ""
},
@ -42,16 +48,25 @@ export class AgentService {
];
}
async processActions(selected: vscode.QuickPickItem): Promise<void> {
switch (selected.label) {
async processActions(): Promise<void> {
let agentsActions: vscode.QuickPickItem[] = this.app.agentService.getActions();
let actionSelected = await vscode.window.showQuickPick(agentsActions);
if (!actionSelected) return
switch (actionSelected.label) {
case this.app.configuration.getUiText(UI_TEXT_KEYS.selectStartAgent):
await this.pickAndSelectAgent(this.app.configuration.agents_list);
break;
case this.app.configuration.getUiText(UI_TEXT_KEYS.addAgent):
await this.addAgent(this.app.configuration.agents_list, SETTING_NAME_FOR_LIST.AGENTS);
break;
case this.app.configuration.getUiText(UI_TEXT_KEYS.editAgent):
this.editAgent(this.app.configuration.agents_list);
break;
case this.app.configuration.getUiText(UI_TEXT_KEYS.copyAgent):
this.copyAgent();
break;
case this.app.configuration.getUiText(UI_TEXT_KEYS.deleteAgent):
await this.deleteAgent(this.app.configuration.agents_list, SETTING_NAME_FOR_LIST.AGENTS);
await this.deleteAgent();
break;
case this.app.configuration.getUiText(UI_TEXT_KEYS.viewAgentDetails):
await this.viewAgent(this.app.configuration.agents_list);
@ -120,70 +135,27 @@ export class AgentService {
}
async addAgent(agentsList: Agent[], settingName: string): Promise<void> {
let name = await Utils.getValidatedInput(
'name for your agent (required)',
(input) => input.trim() !== '',
5,
{
placeHolder: 'Enter a user friendly name for your agent (required)',
value: ''
}
);
if (name === undefined) {
vscode.window.showInformationMessage("Agent addition cancelled.");
return;
}
name = this.app.modelService.sanitizeInput(name);
this.app.llamaWebviewProvider.showAgentEditor();
this.app.llamaWebviewProvider.addEditAgent({name: "", description: "", systemInstruction: [], tools: []})
}
let description = await vscode.window.showInputBox({
placeHolder: 'description for the agent - what is the purpose, when to select etc. ',
prompt: 'Enter description for the agent.',
value: ''
});
description = this.app.modelService.sanitizeInput(description || '');
// Collect system instruction lines
let systemInstruction: string[] = [];
let line: string | undefined;
do {
line = await vscode.window.showInputBox({
placeHolder: 'Enter a line for system instruction (empty to finish)',
prompt: 'System instruction line',
value: ''
});
if (line && line.trim() !== '') {
systemInstruction.push(this.app.modelService.sanitizeInput(line));
}
} while (line && line.trim() !== '');
if (systemInstruction.length === 0) {
vscode.window.showWarningMessage("No system instruction provided. Agent may not function properly.");
}
// Select tools
selectTools = async (currentTools: string[]): Promise<string[]> => {
const availableTools = Array.from(this.app.tools.toolsFunc.keys()).map(tool => ({
label: tool,
picked: true // default all
picked: currentTools.includes(tool)
}));
const selectedToolsItems = await vscode.window.showQuickPick(availableTools, {
canPickMany: true,
placeHolder: 'Select tools for the agent (Ctrl+click to select multiple)'
placeHolder: 'Select tools for the agent'
});
const tools = selectedToolsItems ? selectedToolsItems.map(item => item.label) : Array.from(this.app.tools.toolsFunc.keys());
let newAgent: Agent = {
name: name,
description: description,
systemInstruction: systemInstruction,
tools: tools
};
await this.persistAgent(newAgent, agentsList, settingName);
return tools;
}
private async persistAgent(newAgent: Agent, agentsList: Agent[], settingName: string): Promise<void> {
private async persistNewAgent(newAgent: Agent, agentsList: Agent[], settingName: string, confirmMessage: string): Promise<void> {
let agentDetails = this.getAgentDetailsAsString(newAgent);
const shouldAddAgent = await Utils.confirmAction("A new agent will be added. Do you want to add the agent?", agentDetails);
const shouldAddAgent = await Utils.confirmAction(confirmMessage, agentDetails);
if (shouldAddAgent) {
agentsList.push(newAgent);
@ -192,7 +164,40 @@ export class AgentService {
}
}
async deleteAgent(agentsList: Agent[], settingName: string): Promise<void> {
private async persistEditedAgent(editedAgent: Agent, agentsList: Agent[], settingName: string): Promise<void> {
let agentDetails = this.getAgentDetailsAsString(editedAgent);
const shouldAddAgent = await Utils.confirmAction("Do you want to update agent?", agentDetails);
if (shouldAddAgent) {
let agentExisting = agentsList.find(agn => agn.name.trim() == editedAgent.name.trim())
if (agentExisting){
agentExisting.description = editedAgent.description
agentExisting.systemInstruction = editedAgent.systemInstruction
agentExisting.tools = editedAgent.tools
this.app.configuration.updateConfigValue(settingName, agentsList);
vscode.window.showInformationMessage("The agent is updated: " + agentExisting.name);
} else {
vscode.window.showWarningMessage("The agent to update is not found!")
}
}
}
addUpdateAgent = async (agentToAddUpdate: Agent) => {
let agentsList = this.app.configuration.agents_list;
if (agentsList.findIndex(agn => agn.name.trim() == agentToAddUpdate.name.trim()) == -1){
await this.persistNewAgent(
agentToAddUpdate,
agentsList,
SETTING_NAME_FOR_LIST.AGENTS,
"A new agent will be added. Do you want to add the agent?"
);
} else {
await this.persistEditedAgent(agentToAddUpdate, agentsList, SETTING_NAME_FOR_LIST.AGENTS)
}
}
async deleteAgent(): Promise<void> {
let agentsList: Agent[] = this.app.configuration.agents_list;
const agentsItems: QuickPickItem[] = this.getStandardQpList(agentsList, "");
const agentItem = await vscode.window.showQuickPick(agentsItems);
if (agentItem) {
@ -202,12 +207,66 @@ export class AgentService {
);
if (shouldDeleteAgent) {
agentsList.splice(agentIndex, 1);
this.app.configuration.updateConfigValue(settingName, agentsList);
this.app.configuration.updateConfigValue(SETTING_NAME_FOR_LIST.AGENTS, agentsList);
vscode.window.showInformationMessage("The agent is deleted.")
}
}
}
async editAgent(agentsList: Agent[]): Promise<void> {
const agentsItems: QuickPickItem[] = this.getStandardQpList(agentsList, "");
const agentItem = await vscode.window.showQuickPick(agentsItems);
if (agentItem) {
let agentIndex = parseInt(agentItem.label.split(". ")[0], 10) - 1;
this.app.llamaWebviewProvider.showAgentEditor();
setTimeout(() => {
this.app.llamaWebviewProvider.addEditAgent(agentsList[agentIndex])
}, 500);
}
}
async copyAgent(): Promise<void> {
let agentsList = this.app.configuration.agents_list
let allAgents = agentsList.concat(PREDEFINED_LISTS.get(PREDEFINED_LISTS_KEYS.AGENTS) as Agent[]);
let agentsItems: QuickPickItem[] = this.getStandardQpList(agentsList, "");
agentsItems = agentsItems.concat(this.getStandardQpList(PREDEFINED_LISTS.get(PREDEFINED_LISTS_KEYS.AGENTS) as Agent[], "(predefined) ", agentsList.length));
let agentItem = await vscode.window.showQuickPick(agentsItems);
if (agentItem) {
let agentIndex = parseInt(agentItem.label.split(". ")[0], 10) - 1;
this.app.llamaWebviewProvider.showAgentEditor();
const selectedAgent = allAgents[agentIndex];
const newAgent: Agent = {
name: "Copy of " + agentItem.label,
description: selectedAgent.description,
systemInstruction: selectedAgent.systemInstruction,
tools: selectedAgent.tools
}
setTimeout(() => {
this.app.llamaWebviewProvider.addEditAgent(newAgent)
}, 500);
}
// let agentsItems: QuickPickItem[] = this.getStandardQpList(agentsList, "");
// agentsItems = agentsItems.concat(this.getStandardQpList(PREDEFINED_LISTS.get(PREDEFINED_LISTS_KEYS.AGENTS) as Agent[], "(predefined) ", agentsList.length));
// const agentItem = await vscode.window.showQuickPick(agentsItems);
// if (agentItem) {
// let agentIndex = parseInt(agentItem.label.split(". ")[0], 10) - 1;
// this.app.llamaWebviewProvider.showAgentEditor();
// const selectedAgent = agentsList[agentIndex];
// const newAgent: Agent = {
// name: "Copyt of " + selectedAgent.name,
// description: selectedAgent.description,
// systemInstruction: selectedAgent.systemInstruction,
// tools: selectedAgent.tools
// }
// setTimeout(() => {
// this.app.llamaWebviewProvider.addEditAgent(newAgent)
// }, 500);
// }
}
async viewAgent(agentsList: Agent[]): Promise<void> {
let allAgents = agentsList.concat(PREDEFINED_LISTS.get(PREDEFINED_LISTS_KEYS.AGENTS) as Agent[]);
let agentsItems: QuickPickItem[] = this.getStandardQpList(agentsList, "");
@ -277,7 +336,7 @@ export class AgentService {
// Sanitize imported agent
this.sanitizeAgent(newAgent);
await this.persistAgent(newAgent, agentsList, settingName);
await this.persistNewAgent(newAgent, agentsList, settingName,"A new agent will be added. Do you want to add the agent?");
}
private sanitizeAgent(agent: Agent): void {
@ -309,4 +368,21 @@ export class AgentService {
}
return items;
}
resetEditedAgentTools = () => {
this.editedAgentTools.clear();
this.app.llamaWebviewProvider.updateContextFilesInfo();
}
addEditedAgentTools = (toolName: string, toolDescription: string) => {
this.editedAgentTools.set(toolName, toolDescription);
}
removeEditedAgentTools = (toolName: string) => {
this.editedAgentTools.delete(toolName);
}
getEditedAgentTools = () => {
return this.editedAgentTools;
}
}

View file

@ -23,7 +23,7 @@ export class ChatService {
const chat = await vscode.window.showQuickPick(chatsItems);
if (chat) {
let futureChat: Chat;
futureChat = chatsList[parseInt(chat.label.split(". ")[0], 10) - 1]
futureChat = chatsList[parseInt(chat.label.split(". ")[0], 10) - 1]
if(!futureChat){
vscode.window.showWarningMessage("No chat selected.");
return;

View file

@ -19,7 +19,8 @@ export class ModelService {
this.strategies = {
local: this.app.localModelStrategy,
external: this.app.externalModelStrategy,
hf: this.app.hfModelStrategy
hf: this.app.hfModelStrategy,
oaiComp: this.app.openAiCompModelStrategy
};
}
@ -31,6 +32,7 @@ export class ModelService {
UI_TEXT_KEYS.addLocalCompletionModel,
UI_TEXT_KEYS.addExternalCompletionModel,
UI_TEXT_KEYS.addCompletionModelFromHuggingface,
UI_TEXT_KEYS.addCompletionOpenAiCompModel,
UI_TEXT_KEYS.viewCompletionModelDetails,
UI_TEXT_KEYS.deleteCompletionModel,
UI_TEXT_KEYS.exportCompletionModel,
@ -42,6 +44,7 @@ export class ModelService {
UI_TEXT_KEYS.addLocalChatModel,
UI_TEXT_KEYS.addExternalChatModel,
UI_TEXT_KEYS.addChatModelFromHuggingface,
UI_TEXT_KEYS.addChatOpenAiCompModel,
UI_TEXT_KEYS.viewChatModelDetails,
UI_TEXT_KEYS.deleteChatModel,
UI_TEXT_KEYS.exportChatModel,
@ -53,6 +56,7 @@ export class ModelService {
UI_TEXT_KEYS.addLocalEmbeddingsModel,
UI_TEXT_KEYS.addExternalEmbeddingsModel,
UI_TEXT_KEYS.addEmbeddingsModelFromHuggingface,
UI_TEXT_KEYS.addEmbeddingsOpenAiCompModel,
UI_TEXT_KEYS.viewEmbeddingsModelDetails,
UI_TEXT_KEYS.deleteEmbeddingsModel,
UI_TEXT_KEYS.exportEmbeddingsModel,
@ -64,6 +68,7 @@ export class ModelService {
UI_TEXT_KEYS.addLocalToolsModel,
UI_TEXT_KEYS.addExternalToolsModel,
UI_TEXT_KEYS.addToolsModelFromHuggingface,
UI_TEXT_KEYS.addToolsOpenAiCompModel,
UI_TEXT_KEYS.viewToolsModelDetails,
UI_TEXT_KEYS.deleteToolsModel,
UI_TEXT_KEYS.exportToolsModel,
@ -107,6 +112,9 @@ export class ModelService {
case 'addHf':
await this.addModel(type, 'hf');
break;
case 'addOaiComp':
await this.addModel(type, 'oaiComp');
break;
case 'delete':
await this.deleteModel(details.modelsList, details.modelsListSettingName);
break;
@ -130,6 +138,7 @@ export class ModelService {
addLocal: this.app.configuration.getUiText(UI_TEXT_KEYS[`addLocal${typeStr}Model` as keyof typeof UI_TEXT_KEYS]) ?? "",
addExternal: this.app.configuration.getUiText(UI_TEXT_KEYS[`addExternal${typeStr}Model` as keyof typeof UI_TEXT_KEYS]) ?? "",
addHf: this.app.configuration.getUiText(UI_TEXT_KEYS[`add${typeStr}ModelFromHuggingface` as keyof typeof UI_TEXT_KEYS]) ?? "",
addOaiComp: this.app.configuration.getUiText(UI_TEXT_KEYS[`add${typeStr}OpenAiCompModel` as keyof typeof UI_TEXT_KEYS]) ?? "",
view: this.app.configuration.getUiText(UI_TEXT_KEYS[`view${typeStr}ModelDetails` as keyof typeof UI_TEXT_KEYS]) ?? "",
delete: this.app.configuration.getUiText(UI_TEXT_KEYS[`delete${typeStr}Model` as keyof typeof UI_TEXT_KEYS]) ?? "",
export: this.app.configuration.getUiText(UI_TEXT_KEYS[`export${typeStr}Model` as keyof typeof UI_TEXT_KEYS]) ?? "",
@ -142,6 +151,7 @@ export class ModelService {
let allModels = modelsList.concat(PREDEFINED_LISTS.get(type) as LlmModel[])
let modelsItems: QuickPickItem[] = this.getModels(modelsList, "", true);
modelsItems = modelsItems.concat(this.getModels(PREDEFINED_LISTS.get(type) as LlmModel[], "(predefined) ", true, modelsList.length));
modelsItems = modelsItems.concat(this.getModels(PREDEFINED_LISTS.get(type) as LlmModel[], "(predefined) ", true, modelsList.length));
const launchToEndpoint = new Map([
["launch_completion", "endpoint"],
@ -188,7 +198,7 @@ export class ModelService {
await this.app.persistence.setValue(this.getSelectedProp(type), model);
}
public async addModel(type: ModelType, kind: 'local' | 'external' | 'hf'): Promise<void> {
public async addModel(type: ModelType, kind: 'local' | 'external' | 'hf' | 'oaiComp'): Promise<void> {
const details = this.getTypeDetails(type);
const strategy = this.strategies[kind];
if (strategy) {

View file

@ -0,0 +1,171 @@
import * as vscode from "vscode";
import { QuickPickItem } from "vscode";
import { Application } from "../application";
import { IAddStrategy, LlmModel, ModelTypeDetails } from "../types";
import { Utils } from "../utils";
import * as axios from "axios";
import { OPENAI_COMP_PROVIDERS, OpenAiProvidersKeys, SETTING_TO_MODEL_TYPE } from "../constants";
interface OpenAiCompModel {
name: string;
id: string;
description: string;
context_length: string;
pricing: {
completion: number;
prompt: number;
}
}
export class OpenAiCompModelStrategy implements IAddStrategy {
private app: Application;
constructor(app: Application) {
this.app = app;
}
add = async (details: ModelTypeDetails): Promise<void> => {
const modelType = SETTING_TO_MODEL_TYPE[details.modelsListSettingName];
const openAiCompProviders: QuickPickItem[] = [];
for (const [key, value] of Object.entries(OPENAI_COMP_PROVIDERS)) {
openAiCompProviders.push({
label: key,
description: value
});
}
const selProvider = await vscode.window.showQuickPick(openAiCompProviders);
if (selProvider && selProvider.label) {
//if custom - ask for endpoint
let endpoint = selProvider.description??""
let isKeyRequired = true;
if (selProvider.label == OpenAiProvidersKeys.Custom){
endpoint = await vscode.window.showInputBox({
placeHolder: 'endpoint (URL to the API) of an OpenAI compatible provider',
prompt: 'example: http://localhost:8080 or https://openrauter.ai/api'
})??""
isKeyRequired = await Utils.confirmAction(`Is API key required for this endpoint (${endpoint})?`, "");
}
if (!endpoint){
vscode.window.showWarningMessage("Endpoint is not provided!")
return;
}
const providerModels: QuickPickItem[] = [];
const models = await this.getModels(endpoint);
if (models.length == 0) {
vscode.window.showInformationMessage("No models are found.")
return
}
for (let mdl of models) {
providerModels.push({
label: mdl.name,
description: mdl.context_length + " context | $" + (mdl.pricing?.prompt*1000000).toFixed(2) + "/M input tokens | $" + (mdl.pricing?.completion*1000000).toFixed(2) + "/M output tokens",
detail: mdl.description + " | "+ mdl.id,
});
}
const selModel = await vscode.window.showQuickPick(providerModels);
if (!selModel){
vscode.window.showWarningMessage("No model is selected!")
return;
}
let aiModel = selModel.detail
if(aiModel) aiModel = aiModel.split("|").slice(-1)[0]?.trim()
let provider = selProvider.label
if (provider.endsWith("...")) provider = provider.slice(0,-3)
let newModel: LlmModel = {
name: provider + ": " + selModel.label,
localStartCommand: "",
endpoint: endpoint,
aiModel: aiModel == "undefined" ? "" : aiModel??"",
isKeyRequired: isKeyRequired
};
const shouldAddModel = await Utils.confirmAction("You have entered:",
this.getModelDetailsAsString(newModel) +
"\n\nDo you want to add a model with these properties?"
);
if (shouldAddModel) {
let shouldOverwrite = false;
[newModel.name, shouldOverwrite] = await this.getUniqueModelName(details.modelsList, newModel);
if (!newModel.name) {
vscode.window.showInformationMessage("The model was not added as the name was not provided.")
return;
}
if (shouldOverwrite) {
const index = details.modelsList.findIndex(model => model.name === newModel.name);
if (index !== -1) {
details.modelsList.splice(index, 1);
}
}
details.modelsList.push(newModel);
this.app.configuration.updateConfigValue(details.modelsListSettingName, details.modelsList);
vscode.window.showInformationMessage("The model is added: " + newModel.name)
const shouldSelect = await Utils.confirmAction("Do you want to select/start the newly added model?", "");
if (shouldSelect) {
await this.app.modelService.selectStartModel(newModel, modelType, details);
}
}
}
}
private async getModels(endpoint: string): Promise<OpenAiCompModel[]> {
let hfEndpoint = Utils.trimTrailingSlash(endpoint) +"/v1/models";
try {
let result = await axios.default.get(
`${Utils.trimTrailingSlash(hfEndpoint)}`
);
let models: OpenAiCompModel[] = [];
let modelsList: OpenAiCompModel[] = []
if (result && result.data && result.data.models) modelsList = result.data.models
else if (result && result.data && result.data.data) modelsList = result.data.data
if (modelsList.length > 0){
for(let mdl of modelsList){
models.push(mdl)
}
}
return models;
} catch (error){
vscode.window.showErrorMessage("Error getting provider models): " + error)
return [];
}
}
private sanitizeInput(input: string): string {
return input ? input.trim() : '';
}
private async getUniqueModelName(modelsList: LlmModel[], newModel: LlmModel): Promise<[string, boolean]> {
let uniqueName = newModel.name;
let shouldOverwrite = false;
let modelSameName = modelsList.find(model => model.name === uniqueName);
while (uniqueName && !shouldOverwrite && modelSameName !== undefined) {
shouldOverwrite = await Utils.confirmAction("A model with the same name already exists. Do you want to overwrite the existing model?",
"Existing model:\n" +
this.getModelDetailsAsString(modelSameName) +
"\n\nNew model:\n" +
this.getModelDetailsAsString(newModel)
);
if (!shouldOverwrite) {
uniqueName = (await vscode.window.showInputBox({
placeHolder: 'a unique name for your new model',
prompt: 'Enter a unique name for your new model. Leave empty to cancel entering.',
value: newModel.name
})) ?? "";
uniqueName = this.sanitizeInput(uniqueName);
if (uniqueName) modelSameName = modelsList.find(model => model.name === uniqueName);
}
}
return [uniqueName, shouldOverwrite]
}
private getModelDetailsAsString(model: LlmModel): string {
return "name: " + model.name +
"\nlocal start command: " + model.localStartCommand +
"\nendpoint: " + model.endpoint +
"\nmodel name for provider: " + model.aiModel +
"\napi key required: " + model.isKeyRequired
}
}

View file

@ -11,7 +11,7 @@ export class Tools {
private app: Application;
toolsFunc: ToolsMap = new Map();
toolsFuncDesc: ToolsMap = new Map();
tools: any[] = [];
private tools: any[] = [];
vscodeTools: any[] = [];
vscodeToolsSelected: Map<string, boolean> = new Map();
@ -652,6 +652,10 @@ export class Tools {
}
getTools = () => {
return this.tools;
}
selectTools = async () => {
// Define items with initial selection state
const toolItems: vscode.QuickPickItem[] = []

View file

@ -189,4 +189,10 @@ export const translations: string[][] = [
["Import", "Импортирай", "Importieren", "Импортировать", "Importar", "导入", "Importer"],
["Enter agent command name", "Въведете име на команда на агент", "Agentenbefehlsnamen eingeben", "Введите имя команды агента", "Ingresar nombre de comando de agente", "输入代理命令名称", "Entrer le nom de la commande de l'agent"],
["RAG", "RAG", "RAG", "RAG", "RAG", "RAG", "RAG"],
["Add completion model from OpenAI compatible provider...", "Добавяне на модел за допълване от съвместим с OpenAI доставчик...", "Ergänzungsmodell von einem mit OpenAI kompatiblen Anbieter hinzufügen...", "Добавить модель завершения от совместимого с OpenAI поставщика...", "Añadir modelo de finalización de un proveedor compatible con OpenAI...", "添加来自与 OpenAI 兼容提供程序的补全模型...", "Ajouter un modèle de complétion d'un fournisseur compatible OpenAI..."],
["Add chat model from OpenAI compatible provider...", "Добавяне на чат модел от съвместим с OpenAI доставчик...", "Chat-Modell von einem mit OpenAI kompatiblen Anbieter hinzufügen...", "Добавить чат-модель от совместимого с OpenAI поставщика...", "Añadir modelo de chat de un proveedor compatible con OpenAI...", "添加来自与 OpenAI 兼容提供程序的聊天模型...", "Ajouter un modèle de chat d'un fournisseur compatible OpenAI..."],
["Add embeddings model from OpenAI compatible provider...", "Добавяне на модел за ембединг от съвместим с OpenAI доставчик...", "Einbettungsmodell von einem mit OpenAI kompatiblen Anbieter hinzufügen...", "Добавить модель эмбеддингов от совместимого с OpenAI поставщика...", "Añadir modelo de incrustaciones de un proveedor compatible con OpenAI...", "添加来自与 OpenAI 兼容提供程序的嵌入模型...", "Ajouter un modèle d'incorporations d'un fournisseur compatible OpenAI..."],
["Add tools model from OpenAI compatible provider...", "Добавяне на модел за инструменти от съвместим с OpenAI доставчик...", "Werkzeugmodell von einem mit OpenAI kompatiblen Anbieter hinzufügen...", "Добавить модель инструментов от совместимого с OpenAI поставщика...", "Añadir modelo de herramientas de un proveedor compatible con OpenAI...", "添加来自与 OpenAI 兼容提供程序的工具模型...", "Ajouter un modèle d'outils d'un fournisseur compatible OpenAI..."],
["Edit agent...", "Редактиране на агент...", "Agent bearbeiten...", "Редактировать агента...", "Editar agente...", "编辑代理...", "Modifier l'agent..."],
["Copy agent...", "Копиране на агент...", "Agent kopieren...", "Копировать агента...", "Copiar agente...", "复制代理...", "Copier l'agent..."],
];

View file

@ -321,14 +321,14 @@ export class Utils {
return choice === 'Yes';
}
static showUserChoiceDialog = async (message: string, acceptLable: string): Promise<boolean> => {
static showUserChoiceDialog = async (message: string, acceptLabel: string): Promise<boolean> => {
const choice = await vscode.window.showInformationMessage(
"llama-vscode \n\n" + message,
{ modal: true }, // Makes the dialog modal (blocks interaction until resolved)
acceptLable
acceptLabel
);
return choice === acceptLable;
return choice === acceptLabel;
}
static showYesYesdontaskNoDialog = async (message: string): Promise<[boolean, boolean]> => {
@ -343,6 +343,18 @@ export class Utils {
return [choice === 'Yes' || choice === "Yes, don't ask again", choice === "Yes, don't ask again"];
}
static showYesNoNodontAskDialog = async (message: string, acceptLabel: string): Promise<[boolean, boolean]> => {
const choice = await vscode.window.showInformationMessage(
"llama-vscode \n\n" + message,
{ modal: true }, // Makes the dialog modal (blocks interaction until resolved)
acceptLabel,
'No',
"No, don't ask again"
);
return [choice === acceptLabel, choice === "No, don't ask again"];
}
static showOkDialog = async (message: string) => {
const choice = await vscode.window.showInformationMessage(
"llama-vscode \n\n" + message,

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { AgentView, AIRunnerView, AddEnvView } from './components';
import { AgentView, AIRunnerView, AddEnvView, AgentEditor } from './components';
import { vscode } from './types/vscode';
interface AppProps { }
@ -9,6 +9,7 @@ const App: React.FC<AppProps> = () => {
const noModelSelected = 'No model selected';
const noAgentSelected = 'No agent selected';
const noViewSet = 'agent';
const viewAgentEditor = 'agenteditor'
const viewAiRunner = "airunner"
const viewAddEnv = "addenv"
@ -45,6 +46,8 @@ const App: React.FC<AppProps> = () => {
initialState.view || noViewSet
);
const [contextFiles, setContextFiles] = useState<Map<string, string>>(new Map());
const [agentEditTools, setAgentEditTools] = useState<Map<string, string>>(new Map());
const [agentEditDetails, setAgentEditDetails] = useState<{name:string, description:string, systemInstruction:string}>({name:"", description:"", systemInstruction:""});
// Save state to VS Code whenever it changes
useEffect(() => {
@ -91,6 +94,13 @@ const App: React.FC<AppProps> = () => {
case 'updateContextFiles':
setContextFiles(new Map(message.files || []));
break;
case 'updateAgentEdit':
setAgentEditDetails({name: message.name, description: message.description, systemInstruction: message.systemInstruction});
break;
case 'loadAgent':
setAgentEditDetails({name: message.name, description: message.description, systemInstruction: message.systemInstruction});
setAgentEditTools(new Map(message.tools || []));
break;
default:
break;
}
@ -124,6 +134,11 @@ const App: React.FC<AppProps> = () => {
});
};
const handleShowAgentEditor = () => {
vscode.postMessage({
command: 'showAgentEditor'
});
};
const handleSelectedModels = () => {
vscode.postMessage({
@ -162,18 +177,23 @@ const App: React.FC<AppProps> = () => {
<button
onClick={handleShowAgentView}
className="header-btn secondary"
title="Show Llama Agent"
title="Show Llama Agent View"
>
{"💬"}
</button>
<button
onClick={handleShowAgentEditor}
className="header-btn secondary"
title="Show Edit Agent View"
>
{"✎"}
</button>
</div>
</div>
</div>
{view === noViewSet && (
<div>
{/* Environment Selection Header for Agent View */}
<AgentView
displayText={displayText}
setDisplayText={setDisplayText}
@ -189,6 +209,24 @@ const App: React.FC<AppProps> = () => {
</div>
)}
{view === viewAgentEditor && (
<div>
{/* Environment Selection Header for Agent View */}
<AgentEditor
inputText={inputText}
setInputText={setInputText}
currentToolsModel={currentToolsModel}
currentAgent={currentAgent}
currentState={currentState}
setCurrentState={setCurrentState}
contextFiles={agentEditTools}
setContextFiles={setAgentEditTools}
agentEditDetails={agentEditDetails}
setAgentEditDetails = {setAgentEditDetails}
/>
</div>
)}
{view === viewAiRunner && (
<div className="content">
<AIRunnerView currentChatModel={currentChatModel} />

View file

@ -96,6 +96,36 @@ const AddEnvView: React.FC<AddEnvViewProps> = ({
command: 'deselectCompletionModel'
});
};
const handleMoreCompletionModel = () => {
vscode.postMessage({
command: 'moreCompletionModel'
});
};
const handleMoreChatModel = () => {
vscode.postMessage({
command: 'moreChatModel'
});
};
const handleMoreEmbeddingsModel = () => {
vscode.postMessage({
command: 'moreEmbeddingsModel'
});
};
const handleMoreToolsModel = () => {
vscode.postMessage({
command: 'moreToolsModel'
});
};
const handleMoreAgent = () => {
vscode.postMessage({
command: 'moreAgent'
});
};
const handleDeselectChatModel = () => {
vscode.postMessage({
@ -145,12 +175,28 @@ const AddEnvView: React.FC<AddEnvViewProps> = ({
});
};
const handleShowAgentModel = () => {
const handleShowAgent = () => {
vscode.postMessage({
command: 'showAgentDetails'
});
};
const handleEditAgent = () => {
vscode.postMessage({
command: 'showAgentEditor'
});
vscode.postMessage({
command: 'editSelectedAgent',
agent: currentAgent,
});
vscode.postMessage({
command: 'refreshEditedAgentTool',
agent: currentAgent,
});
};
const handleSelectAgent = () => {
vscode.postMessage({
command: 'selectAgent'
@ -189,6 +235,7 @@ const AddEnvView: React.FC<AddEnvViewProps> = ({
return (
<div>
<h1 style={{ margin: '0 0 20px 0', fontSize: '24px', fontWeight: 'bold' }}>Environment</h1>
<div className="llm-buttons">
<div className="single-button-frame">
<div className="frame-label">Compl Model</div>
@ -211,6 +258,15 @@ const AddEnvView: React.FC<AddEnvViewProps> = ({
</button>
)}
<span className="model-text">{currentCompletionModel}</span>
{currentCompletionModel === noModelSelected && (
<button
onClick={handleMoreCompletionModel}
title={`Add/Delete/View/Export/Import Completion Model`}
className="modern-btn secondary"
>
More
</button>
)}
{currentCompletionModel != noModelSelected && (
<button
onClick={handleShowCompletionModel}
@ -244,6 +300,15 @@ const AddEnvView: React.FC<AddEnvViewProps> = ({
</button>
)}
<span className="model-text">{currentChatModel}</span>
{currentChatModel === noModelSelected && (
<button
onClick={handleMoreChatModel}
title={`Add/Delete/View/Export/Import Chat Model`}
className="modern-btn secondary"
>
More
</button>
)}
{currentChatModel != noModelSelected && (
<button
onClick={handleShowChatModel}
@ -277,6 +342,15 @@ const AddEnvView: React.FC<AddEnvViewProps> = ({
</button>
)}
<span className="model-text">{currentEmbeddingsModel}</span>
{currentEmbeddingsModel === noModelSelected && (
<button
onClick={handleMoreEmbeddingsModel}
title={`Add/Delete/View/Export/Import Embeddings Model`}
className="modern-btn secondary"
>
More
</button>
)}
{currentEmbeddingsModel != noModelSelected && (
<button
onClick={handleShowEmbsModel}
@ -311,6 +385,15 @@ const AddEnvView: React.FC<AddEnvViewProps> = ({
</button>
)}
<span className="model-text">{currentToolsModel}</span>
{currentToolsModel === noModelSelected && (
<button
onClick={handleMoreToolsModel}
title={`Add/Delete/View/Export/Import Tools Model`}
className="modern-btn secondary"
>
More
</button>
)}
{currentToolsModel != noModelSelected && (
<button
onClick={handleShowToolsModel}
@ -344,15 +427,33 @@ const AddEnvView: React.FC<AddEnvViewProps> = ({
</button>
)}
<span className="model-text">{currentAgent}</span>
{currentAgent === noAgentSelected && (
<button
onClick={handleMoreAgent}
title={`Add/Delete/View/Export/Import Agentl`}
className="modern-btn secondary"
>
More
</button>
)}
{currentAgent != noAgentSelected && (
<button
onClick={handleShowAgentModel}
onClick={handleShowAgent}
title={`Show Agent Details`}
className="modern-btn secondary"
>
...
</button>
)}
{currentAgent != noAgentSelected && (
<button
onClick={handleEditAgent}
title={`Show Agent Details`}
className="modern-btn secondary"
>
Edit
</button>
)}
</div>
</div>
<div className="llm-buttons">

View file

@ -0,0 +1,440 @@
import React, { useState, useEffect, useRef } from 'react';
import { vscode } from '../types/vscode';
interface AgentEditorProps {
inputText: string;
setInputText: (text: string) => void;
currentToolsModel: string;
currentAgent: string;
currentState: string;
setCurrentState: (state: string) => void;
contextFiles: Map<string, string>;
setContextFiles: (files: Map<string, string>) => void;
agentEditDetails: {name: string, description: string, systemInstruction: string}
setAgentEditDetails:(agentDetails: {name: string, description: string, systemInstruction: string}) => void
}
const AgentEditor: React.FC<AgentEditorProps> = ({
inputText,
setInputText,
currentToolsModel,
currentAgent,
currentState,
setCurrentState,
contextFiles: agentTools,
setContextFiles: setTools,
agentEditDetails,
setAgentEditDetails
}) => {
const [showFileSelector, setShowFileSelector] = useState<boolean>(false);
const [fileList, setFileList] = useState<string[]>([]);
const [fileFilter, setFileFilter] = useState<string>('');
const [selectedIndex, setSelectedIndex] = useState<number>(0);
// Create refs
const elemSystemPromptRef = useRef<HTMLTextAreaElement>(null);
const elemDescriptionRef = useRef<HTMLTextAreaElement>(null);
const elemNameRef = useRef<HTMLTextAreaElement>(null);
const fileListRef = useRef<HTMLDivElement>(null);
// Auto-focus the textarea when the component mounts
useEffect(() => {
if (elemSystemPromptRef.current) {
elemSystemPromptRef.current.focus();
}
}, []);
// Filter files based on user input
const filteredFiles = fileList.filter(file =>
file.toLowerCase().includes(fileFilter.toLowerCase())
);
// Reset selected index when file list or filter changes
useEffect(() => {
setSelectedIndex(0);
}, [fileList, fileFilter]);
// Auto-scroll to keep selected item visible
useEffect(() => {
if (showFileSelector && fileListRef.current && filteredFiles.length > 0) {
const fileItems = fileListRef.current.querySelectorAll('.file-item');
if (fileItems[selectedIndex]) {
fileItems[selectedIndex].scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
}
}, [selectedIndex, showFileSelector, filteredFiles.length]);
useEffect(() => {
// Listen for messages from the extension
const handleMessage = (event: MessageEvent) => {
const message = event.data;
console.log('Received message from extension:', message);
switch (message.command) {
case 'focusTextarea':
// Focus the textarea when requested by the extension
if (elemSystemPromptRef.current) {
elemSystemPromptRef.current.focus();
}
break;
case 'updateCurrentState':
setCurrentState(message.text || '');
break;
case 'updateFileList':
setFileList(message.files || []);
setShowFileSelector(true);
break;
case 'updateAgentTools':
setTools(new Map(message.files || []));
break;
case 'loadAgent':
setAgentEditDetails({name: message.name, description: message.description, systemInstruction: message.systemInstruction});
setTools(new Map(message.tools || []));
break;
default:
break;
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [setCurrentState, setTools]);
// Function to focus the textarea (can be called from extension)
const focusTextarea = () => {
if (elemSystemPromptRef.current) {
elemSystemPromptRef.current.focus();
}
};
// Expose the focus function to the extension
useEffect(() => {
// @ts-ignore - Adding to window for extension access
window.focusTextarea = focusTextarea;
}, []);
const handlеSaveAgent = () => {
vscode.postMessage({
command: 'saveEditAgent',
name: agentEditDetails.name,
description: agentEditDetails.description,
systemInstruction: agentEditDetails.systemInstruction,
tools: Array.from(agentTools.keys())
});
};
const handleSelectTools = () => {
vscode.postMessage({
command: "configureEditTools",
tools: Array.from(agentTools.keys())
});
}
const handleSelectToolsModel = () => {
vscode.postMessage({
command: 'selectModelWithTools'
});
};
const handleSelectEditAgent = () => {
vscode.postMessage({
command: 'selectEditAgent'
});
}
const handleNewAgent = () => {
vscode.postMessage({
command: 'addEditAgent',
name: "",
description: "",
systemInstruction: [],
tools: []
});
}
const handleCopyAgent = () => {
vscode.postMessage({
command: 'copyAsNewAgent'
});
}
const handleDeleteAgent = () => {
vscode.postMessage({
command: 'deleteAgent'
});
}
const handleFileSelect = (fileLongName: string) => {
// Send the selected file to the extension
setShowFileSelector(false);
setFileFilter('');
vscode.postMessage({
command: 'addEditedAgentTool',
fileLongName: fileLongName
});
if (elemSystemPromptRef.current) {
elemSystemPromptRef.current.focus();
}
};
const handleCancelFileSelect = () => {
setShowFileSelector(false);
setFileFilter('');
if (elemSystemPromptRef.current) {
elemSystemPromptRef.current.focus();
}
};
const handleRemoveTool = (fileLongName: string) => {
vscode.postMessage({
command: 'removeEditedAgentTool',
fileLongName: fileLongName
});
};
const handleOpenContextFile = (fileLongName: string) => {
vscode.postMessage({
command: 'openContextFile',
fileLongName: fileLongName
});
};
// Handle keyboard navigation in file selector
const handleFileSelectorKeyDown = (e: React.KeyboardEvent) => {
if (!showFileSelector || filteredFiles.length === 0) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(prev =>
prev < filteredFiles.length - 1 ? prev + 1 : prev
);
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(prev => prev > 0 ? prev - 1 : 0);
break;
case 'Enter':
e.preventDefault();
if (filteredFiles.length > 0) {
handleFileSelect(filteredFiles[selectedIndex]);
}
break;
case 'Escape':
e.preventDefault();
handleCancelFileSelect();
break;
}
};
// Handle keyboard events in the search input
const handleSearchKeyDown = (e: React.KeyboardEvent) => {
if (!showFileSelector || filteredFiles.length === 0) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
// Focus the file list and set first item as selected
if (fileListRef.current) {
fileListRef.current.focus();
setSelectedIndex(0);
}
break;
case 'ArrowUp':
e.preventDefault();
// Focus the file list and set last item as selected
if (fileListRef.current) {
fileListRef.current.focus();
setSelectedIndex(filteredFiles.length - 1);
}
break;
case 'Enter':
e.preventDefault();
// If there are files, focus the list and select the first one
if (filteredFiles.length > 0) {
if (fileListRef.current) {
fileListRef.current.focus();
setSelectedIndex(0);
}
}
break;
case 'Escape':
e.preventDefault();
handleCancelFileSelect();
break;
}
};
return (
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Main Content */}
<div className="content" style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div className="input-section" style={{ flexShrink: 0 }}>
<div className="input-container">
{/* Page Title */}
<h1 style={{ margin: '0 0 20px 0', fontSize: '24px', fontWeight: 'bold' }}>Agent Editor</h1>
<div style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
<button
onClick={handleSelectEditAgent}
title={`Select Agent to Edit`}
className="modern-btn secondary"
>
Select
</button>
<button
onClick={handleNewAgent}
title={`New Agent`}
className="modern-btn secondary"
>
New
</button>
<button
onClick={handleCopyAgent}
title={`Copy Existing Agent as New`}
className="modern-btn secondary"
>
Copy as New
</button>
<button
onClick={handleDeleteAgent}
title={`Select and Delete Existing Agent`}
className="modern-btn secondary"
>
Delete
</button>
</div>
{/* Context Files */}
{agentTools.size > 0 && (
<div className="context-chips">
{Array.from(agentTools.entries()).map(([toolName, toolDescription]) => (
<div key={toolName} className="context-chip">
<span
className="file-name clickable"
onClick={() => handleOpenContextFile(toolName)}
>
{toolName}
</span>
<button
className="remove-btn"
onClick={() => handleRemoveTool(toolName)}
title={`Remove ${toolName} from context`}
>
×
</button>
</div>
))}
</div>
)}
<span>{'Name (agent identifier)'}</span>
{/* Modern Textarea */}
<textarea
ref={elemNameRef}
value={agentEditDetails.name}
onChange={(e) => setAgentEditDetails({name: e.target.value, description: agentEditDetails.description, systemInstruction: agentEditDetails.systemInstruction})}
placeholder="Enter agent name."
className="modern-textarea"
rows={1}
style={{ height: 'auto', minHeight: '1.5em', resize: 'none' }}
/>
<span>{'Descriptoin'}</span>
{/* Modern Textarea */}
<textarea
ref={elemDescriptionRef}
value={agentEditDetails.description}
onChange={(e) => setAgentEditDetails({name: agentEditDetails.name, description: e.target.value, systemInstruction: agentEditDetails.systemInstruction})}
placeholder="Enter agent description."
className="modern-textarea"
rows={2}
style={{ height: 'auto', minHeight: '3em', resize: 'none' }}
/>
<span>{'System Instruction'}</span>
{/* Modern Textarea */}
<textarea
ref={elemSystemPromptRef}
value={agentEditDetails.systemInstruction}
onChange={(e) => setAgentEditDetails({name: agentEditDetails.name, description: agentEditDetails.description, systemInstruction: e.target.value})}
placeholder="Enter system instructions for the agent."
className="modern-textarea"
rows={10}
style={{ height: 'auto', minHeight: '15em', resize: 'vertical' }}
/>
{/* Input Actions */}
<div className="input-actions">
<div className="input-buttons">
<button
onClick={handleSelectTools}
className="modern-btn secondary"
title="Add file to context"
>
Add Tools
</button>
</div>
<button
onClick={handlеSaveAgent}
className={`modern-btn ${inputText.trim() === '' ? 'secondary' : ''}`}
title={"Saves the agent changes or creates a new agent if the name does not exit."}
>
Save
</button>
</div>
</div>
</div>
</div>
{/* File Selection Dialog */}
{showFileSelector && (
<div className="file-selector-overlay">
<div className="file-selector-dialog">
<div className="file-selector-header">
<h3>Select an item to add to context</h3>
<button onClick={handleCancelFileSelect} className="close-btn">×</button>
</div>
<div className="file-selector-search">
<input
type="text"
placeholder="Filter ..."
value={fileFilter}
onChange={(e) => setFileFilter(e.target.value)}
onKeyDown={handleSearchKeyDown}
autoFocus
/>
</div>
<div
ref={fileListRef}
className="file-selector-list"
onKeyDown={handleFileSelectorKeyDown}
tabIndex={0}
>
{filteredFiles.length > 0 ? (
filteredFiles.map((file, index) => (
<div
key={index}
className={`file-item ${index === selectedIndex ? 'selected' : ''}`}
onClick={() => handleFileSelect(file)}
>
{file}
</div>
))
) : (
<div className="no-files">
{fileFilter ? 'No files match your filter' : 'No files available'}
</div>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default AgentEditor;

View file

@ -321,7 +321,6 @@ const AgentView: React.FC<AgentViewProps> = ({
{/* Modern Header */}
<div className="header">
<div className="header-content">
// TODO Remove this check once you are sure it works fine without it
{!currentToolsModel.includes('No model selected...') && (
<div className="header-left">
<button

View file

@ -1,5 +1,6 @@
export { default as AgentView } from './AgentView';
export { default as AIRunnerView } from './AIRunnerView';
export { default as AddEnvView } from './AddEnvView';
export { default as AgentEditor } from './AgentEditor';