Manage TOML Configuration From VSCode Extension - DBChat Part 8
Hi there! I'm Shrijith Venkatrama, the founder of Hexmos. Right now, I’m building LiveAPI, a super-convenient tool that simplifies engineering workflows by generating awesome API docs from your code in minutes. In this tutorial series, I am on a journey to build for myself DBChat - a simple tool for using AI chat to explore and evolve databases. See previous posts to get more context: Building DBChat - Explore and Evolve Your DB with Simple Chat (Part 1) DBChat: Getting a Toy REPL Going in Golang (Part 2) DBChat Part 3 - Configure , Connect & Dump Databases Chat With Your DB via DBChat & Gemini (Part 4) The Language Server Protocol - Building DBChat (Part 5) Making DBChat VSCode Extension - Ping Pong With LSP Backend (Part 6) Starting a VSCode Extension UI For DBChat (Part 7) Building a TOML Connections Manager UI for DBChat in VSCode Extension In the previous post - we had the skeletons of a simple chat UI and database connections form in DBChat VSCode extension. In this post, I will demonstrate how DBChat extension can manipulate the ~/.dbchat.toml config file's [connections] section to add/update/delete entries. To jog your memory, the config file is supposed to have a structure like this: # DBChat Sample Configuration File # Copy this file to ~/.dbchat.toml and modify as needed [connections] # Format: name = "connection_string" local = "postgresql://postgres:postgres@localhost:5432/postgres" liveapi = "postgresql://user:pwd@ip:5432/db_name" [llm] gemini_key = "the_key" The Results DBChat Connections List: DBChat Add/Edit Connection: For both Edit and Update - we also have a confirmation prompt to avoid mistakes. Handling Create Connection Request The first task is to get the toml extension as follows: npm install @iarna/toml We get a few new imports: import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import * as TOML from '@iarna/toml'; The key structure is a message handler - which will receive events for all the 3 actions: const messageHandler = this._view.webview.onDidReceiveMessage( async (message) => { console.log('Received message:', message); switch (message.command) { case 'saveConnection': console.log('Processing saveConnection command'); const success = await this._saveConnection(message.name, message.connectionString); if (success) { console.log('Connection saved successfully, closing form'); this._showingConnectionForm = false; this._updateView(); } else { console.log('Connection not saved, keeping form open'); } break; case 'cancel': console.log('Processing cancel command'); this._showingConnectionForm = false; this._updateView(); break; case 'editConnection': this._showingConnectionForm = true; this._editingConnection = message.name; // First update the view to show the form await this._updateView(); // Then send the prefill message after a short delay to ensure the form exists setTimeout(() => { this._view.webview.postMessage({ command: 'prefillForm', name: message.name, connectionString: message.connectionString }); }, 100); break; case 'deleteConnection': const choice = await vscode.window.showWarningMessage( `Are you sure you want to delete connection "${message.name}"?`, 'Yes', 'No' ); if (choice === 'Yes') { const deleted = await this._deleteConnection(message.name); if (deleted) { await this._updateView(); // Update view after successful deletion vscode.window.showInformationMessage(`Connection "${message.name}" deleted successfully.`); } } break; } } ); // Add message handler to subscriptions for cleanup context.subscriptions.push(messageHandler); Save Connection is quite simple: private async _saveConnection(name: string, connectionSt
Hi there! I'm Shrijith Venkatrama, the founder of Hexmos. Right now, I’m building LiveAPI, a super-convenient tool that simplifies engineering workflows by generating awesome API docs from your code in minutes.
In this tutorial series, I am on a journey to build for myself DBChat - a simple tool for using AI chat to explore and evolve databases.
See previous posts to get more context:
- Building DBChat - Explore and Evolve Your DB with Simple Chat (Part 1)
- DBChat: Getting a Toy REPL Going in Golang (Part 2)
- DBChat Part 3 - Configure , Connect & Dump Databases
- Chat With Your DB via DBChat & Gemini (Part 4)
- The Language Server Protocol - Building DBChat (Part 5)
- Making DBChat VSCode Extension - Ping Pong With LSP Backend (Part 6)
- Starting a VSCode Extension UI For DBChat (Part 7)
Building a TOML Connections Manager UI for DBChat in VSCode Extension
In the previous post - we had the skeletons of a simple chat UI and database connections form in DBChat VSCode extension.
In this post, I will demonstrate how DBChat extension can manipulate the ~/.dbchat.toml
config file's [connections]
section to add/update/delete entries.
To jog your memory, the config file is supposed to have a structure like this:
# DBChat Sample Configuration File
# Copy this file to ~/.dbchat.toml and modify as needed
[connections]
# Format: name = "connection_string"
local = "postgresql://postgres:postgres@localhost:5432/postgres"
liveapi = "postgresql://user:pwd@ip:5432/db_name"
[llm]
gemini_key = "the_key"
The Results
DBChat Connections List:
DBChat Add/Edit Connection:
For both Edit and Update - we also have a confirmation prompt to avoid mistakes.
Handling Create Connection Request
The first task is to get the toml extension as follows:
npm install @iarna/toml
We get a few new imports:
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
import * as TOML from '@iarna/toml';
The key structure is a message handler - which will receive events for all the 3 actions:
const messageHandler = this._view.webview.onDidReceiveMessage(
async (message) => {
console.log('Received message:', message);
switch (message.command) {
case 'saveConnection':
console.log('Processing saveConnection command');
const success = await this._saveConnection(message.name, message.connectionString);
if (success) {
console.log('Connection saved successfully, closing form');
this._showingConnectionForm = false;
this._updateView();
} else {
console.log('Connection not saved, keeping form open');
}
break;
case 'cancel':
console.log('Processing cancel command');
this._showingConnectionForm = false;
this._updateView();
break;
case 'editConnection':
this._showingConnectionForm = true;
this._editingConnection = message.name;
// First update the view to show the form
await this._updateView();
// Then send the prefill message after a short delay to ensure the form exists
setTimeout(() => {
this._view.webview.postMessage({
command: 'prefillForm',
name: message.name,
connectionString: message.connectionString
});
}, 100);
break;
case 'deleteConnection':
const choice = await vscode.window.showWarningMessage(
`Are you sure you want to delete connection "${message.name}"?`,
'Yes',
'No'
);
if (choice === 'Yes') {
const deleted = await this._deleteConnection(message.name);
if (deleted) {
await this._updateView(); // Update view after successful deletion
vscode.window.showInformationMessage(`Connection "${message.name}" deleted successfully.`);
}
}
break;
}
}
);
// Add message handler to subscriptions for cleanup
context.subscriptions.push(messageHandler);
Save Connection is quite simple:
private async _saveConnection(name: string, connectionString: string): Promise<boolean> {
console.log('Starting _saveConnection with:', { name, connectionString });
try {
const configPath = path.join(os.homedir(), 'dbchat.toml');
console.log('Config path:', configPath);
let config: any = {
connections: {},
llm: {}
};
console.log('Initial config structure:', config);
// Read existing config if it exists
try {
console.log('Attempting to read existing config file...');
const fileContent = await fs.readFile(configPath, 'utf-8');
console.log('Existing file content:', fileContent);
console.log('Parsing TOML content...');
config = TOML.parse(fileContent);
console.log('Parsed config:', config);
// Ensure connections section exists
config.connections = config.connections || {};
console.log('Config after ensuring connections exist:', config);
} catch (error: any) {
console.log('Error reading config:', error);
if (error.code !== 'ENOENT') {
console.error('Unexpected error reading config:', error);
throw error;
}
console.log('Config file does not exist, will create new one');
}
// Check if connection already exists
if (config.connections[name]) {
console.log(`Connection "${name}" already exists, showing confirmation dialog`);
const choice = await vscode.window.showWarningMessage(
`Connection "${name}" already exists. Do you want to overwrite it?`,
'Yes',
'No'
);
console.log('User choice for overwrite:', choice);
if (choice !== 'Yes') {
console.log('User declined to overwrite, returning false');
return false;
}
}
// Update the connection
config.connections[name] = connectionString;
console.log('Updated config:', config);
// Convert config to TOML and write back to file
console.log('Converting config to TOML...');
const tomlContent = TOML.stringify(config);
console.log('Generated TOML content:', tomlContent);
// Preserve the header comments
const finalContent = `# DBChat Sample Configuration File
# Copy this file to ~/.dbchat.toml and modify as needed
${tomlContent}`;
console.log('Final content to write:', finalContent);
console.log('Writing to file...');
await fs.writeFile(configPath, finalContent, 'utf-8');
console.log('File written successfully');
// Update view immediately after successful file write
this._showingConnectionForm = false;
console.log('Form hidden, updating view');
this._updateView();
await vscode.window.showInformationMessage(`Connection "${name}" saved successfully!`, { modal: false });
return true;
} catch (error) {
console.error('Error in _saveConnection:', error);
if (error instanceof Error) {
console.error('Error stack:', error.stack);
}
await vscode.window.showErrorMessage(`Failed to save connection: ${error}`);
return false;
}
}
List Connections
private async _getConnections(): Promise<{[key: string]: string}> {
try {
const configPath = path.join(os.homedir(), 'dbchat.toml');
const fileContent = await fs.readFile(configPath, 'utf-8');
const config = TOML.parse(fileContent);
return config.connections || {};
} catch (error) {
console.error('Error reading connections:', error);
return {};
}
}
Delete Connections
private async _deleteConnection(name: string): Promise<boolean> {
try {
const configPath = path.join(os.homedir(), 'dbchat.toml');
const fileContent = await fs.readFile(configPath, 'utf-8');
const config = TOML.parse(fileContent);
if (!config.connections || !config.connections[name]) {
await vscode.window.showErrorMessage(`Connection "${name}" not found.`);
return false;
}
delete config.connections[name];
const tomlContent = TOML.stringify(config);
const finalContent = `# DBChat Sample Configuration File
# Copy this file to ~/.dbchat.toml and modify as needed
${tomlContent}`;
await fs.writeFile(configPath, finalContent, 'utf-8');
// Show message after file operations are complete
vscode.window.showInformationMessage(`Connection "${name}" deleted successfully.`);
return true;
} catch (error) {
console.error('Error deleting connection:', error);
vscode.window.showErrorMessage(`Failed to delete connection: ${error}`);
return false;
}
}
That's about it for this post. With this structure - we implemented a basic database connection listing, add, delete & update operations.
Next Steps
Since we have a primitive database configuration mechanism, next we will aim to enable connecting to a particular configuration, obtaining schema, chat with DB, etc - using the golang LSP.
What's Your Reaction?