Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 43 additions & 30 deletions src/main/adapters/cli.adapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import { BrowserWindow } from 'electron';
import { CliMessage } from '../../types/backend';
import { CliMessage, CliProcessEnvironment } from '../../types/backend';

const createProcessEnvironment = (environment?: CliProcessEnvironment) => ({
...process.env,
...(environment?.DBT_ALLOW_EXPERIMENTAL_ADAPTERS === 'true'
? { DBT_ALLOW_EXPERIMENTAL_ADAPTERS: 'true' }
: {}),
});

class CliAdapter {
private process: ChildProcessWithoutNullStreams | null = null;
Expand All @@ -9,17 +16,27 @@ class CliAdapter {
return this.process;
}

async runCommandWithoutStreaming(command: string, args?: string[]) {
async runCommandWithoutStreaming(
command: string,
args?: string[],
environment?: CliProcessEnvironment,
) {
return new Promise<void>((resolve, reject) => {
if (this.process) {
reject(new Error('A command is already running. Please wait.'));
return;
}

if (args && args.length > 0) {
this.process = spawn(command, args, { shell: false });
this.process = spawn(command, args, {
shell: false,
env: createProcessEnvironment(environment),
});
} else {
this.process = spawn(command, { shell: true });
this.process = spawn(command, {
shell: true,
env: createProcessEnvironment(environment),
});
}

// Drain stdout/stderr to avoid the child process blocking when buffers fill.
Expand All @@ -42,7 +59,12 @@ class CliAdapter {
});
}

runCommand(mainWindow: BrowserWindow, command: string, args?: string[]) {
runCommand(
mainWindow: BrowserWindow,
command: string,
args?: string[],
environment?: CliProcessEnvironment,
) {
return new Promise<void>((resolve, reject) => {
if (this.process) {
reject(new Error('A command is already running. Please wait.'));
Expand All @@ -52,9 +74,15 @@ class CliAdapter {
mainWindow.webContents.send('cli:clear');

if (args && args.length > 0) {
this.process = spawn(command, args, { shell: false });
this.process = spawn(command, args, {
shell: false,
env: createProcessEnvironment(environment),
});
} else {
this.process = spawn(command, { shell: true });
this.process = spawn(command, {
shell: true,
env: createProcessEnvironment(environment),
});
}

this.messageHandler(
Expand Down Expand Up @@ -82,34 +110,19 @@ class CliAdapter {
});

this.process.on('close', (code) => {
// Send exit event first
mainWindow.webContents.send('cli:exit', code);

// Always send done event for frontend to know command completed
mainWindow.webContents.send('cli:done');

// Reset process
this.process = null;

// Handle promise resolution
if (code === 0) {
this.messageHandler(
{
type: 'success',
message: `Command executed successfully.`,
},
mainWindow,
);
mainWindow.webContents.send('cli:exit', code);
mainWindow.webContents.send('cli:done', code);
resolve();
} else {
// Don't call messageHandler with error type here since it calls stopCommand
// Just add the exit code message directly
mainWindow.webContents.send(
'cli:output',
`Process exited with code ${code}`,
);
const exitMessage = `Process exited with code ${code}`;
mainWindow.webContents.send('cli:error', exitMessage);
mainWindow.webContents.send('cli:exit', code);
mainWindow.webContents.send('cli:done', code);
reject(new Error(`Process exited with error code ${code}`));
}

this.process = null;
});

this.process.on('error', (err) => {
Expand Down
9 changes: 8 additions & 1 deletion src/main/ipcHandlers/cli.ipcHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ipcMain, BrowserWindow } from 'electron';
import { CliAdapter } from '../adapters';
import { CliProcessEnvironment } from '../../types/backend';

const cliAdapter = new CliAdapter();

Expand Down Expand Up @@ -38,11 +39,17 @@ const registerCliHandlers = (mainWindow: BrowserWindow) => {
args: {
command: string;
args?: string[];
environment?: CliProcessEnvironment;
cb?: (message: string) => void;
},
) => {
try {
await cliAdapter.runCommand(mainWindow, args.command, args.args);
await cliAdapter.runCommand(
mainWindow,
args.command,
args.args,
args.environment,
);
return { success: true };
} catch (error: any) {
const errorMessage =
Expand Down
62 changes: 57 additions & 5 deletions src/main/ipcHandlers/settings.ipcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { initializeDataStorage } from '../utils/setupHelpers';
import { FileDialogProperties, SettingsType } from '../../types/backend';
import { SettingsService } from '../services';
import { SettingsChannels } from '../../types/ipc';
import { DbtVersionManagerService } from '../services/dbtVersionManager.service';
import { DbtCoreVersionService } from '../services/dbtCoreVersion.service';

const handlerChannels: SettingsChannels[] = [
'settings:load',
Expand All @@ -19,6 +19,18 @@ const handlerChannels: SettingsChannels[] = [
'settings:restart',
'settings:getBasename',
'settings:getDirname',
'dbt:versions:list',
'dbt:installed:get',
'dbt:versionChange:plan',
'dbt:versionChange:install',
'dbt:compatibility:check',
'dbt:adapters:active',
'dbt:adapters:check',
'dbt:packages:installed',
'dbt:package:installLatest',
'dbt:package:uninstall',
'dbt:packageVersions:list',
'dbt:packageVersion:install',
];

const removeSettingsIpcHandlers = () => {
Expand Down Expand Up @@ -133,16 +145,56 @@ const registerSettingsHandlers = (mainWindow: BrowserWindow) => {
return SettingsService.installSqlGlot();
});

ipcMain.handle('dbt:versions:list', async () => {
return DbtVersionManagerService.listDbtCoreVersions();
ipcMain.handle('dbt:versions:list', async (_event, request) => {
return DbtCoreVersionService.listDbtCoreVersions(request);
});

ipcMain.handle('dbt:installed:get', async () => {
return DbtCoreVersionService.getInstalledDbtCore();
});

ipcMain.handle('dbt:versionChange:plan', async (_event, request) => {
return DbtCoreVersionService.planVersionChange(request);
});

ipcMain.handle('dbt:versionChange:install', async (_event, request) => {
return DbtCoreVersionService.installVersionChange(request);
});

ipcMain.handle('dbt:compatibility:check', async () => {
return DbtCoreVersionService.checkCurrentProjectCompatibility();
});

ipcMain.handle('dbt:adapters:active', async (_event, request) => {
return DbtCoreVersionService.getActiveAdapterCapabilities(
request?.projectPath,
);
});

ipcMain.handle('dbt:adapters:check', async (_event, request) => {
return DbtCoreVersionService.checkProjectAdapterCompatibility(
request?.projectPath,
);
});

ipcMain.handle('dbt:packages:installed', async () => {
return DbtCoreVersionService.getInstalledPackages();
});

ipcMain.handle('dbt:package:installLatest', async (_event, request) => {
return DbtCoreVersionService.installLatestPackage(request);
});

ipcMain.handle('dbt:package:uninstall', async (_event, request) => {
return DbtCoreVersionService.uninstallPackage(request);
});

ipcMain.handle('dbt:packageVersions:list', async (_event, req) => {
return DbtVersionManagerService.listPackageVersions(req?.packageName);
return DbtCoreVersionService.listPackageVersions(req?.packageName);
});

ipcMain.handle('dbt:packageVersion:install', async (_event, req) => {
return DbtVersionManagerService.installPackageVersion(req);
return DbtCoreVersionService.installPackageVersion(req);
});
};

Expand Down
35 changes: 25 additions & 10 deletions src/main/services/agent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ import type {
} from '../../types/agentEvents';
import { getUserMessageLimitError } from '../../types/agentEvents';
import { toError } from '../utils/errorSerializer';
import {
getToolResultError,
isToolResultFailure,
} from '../../shared/toolResult';

// ─── AI Settings ─────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -1214,23 +1218,28 @@ COMBINED SUMMARY:`,
});
break;
case 'tool-result': {
const toolOutput =
(chunk as any).output ?? (chunk as any).result;
const toolFailed = isToolResultFailure(toolOutput);
collectedToolCalls.push({
toolName: chunk.toolName,
toolCallId: chunk.toolCallId,
input: (chunk as any).input ?? (chunk as any).args,
output: (chunk as any).output ?? (chunk as any).result,
output: toolOutput,
stepNumber: currentStepNumber >= 0 ? currentStepNumber : 0,
status: 'done',
status: toolFailed ? 'error' : 'done',
});
const part = collectedParts.find(
(p) =>
p.type === 'tool-call' &&
p.toolCallId === chunk.toolCallId,
);
if (part) {
part.result =
(chunk as any).output ?? (chunk as any).result;
part.status = 'done';
part.result = toolOutput;
part.error = toolFailed
? getToolResultError(toolOutput)
: undefined;
part.status = toolFailed ? 'error' : 'done';
}
break;
}
Expand Down Expand Up @@ -1298,21 +1307,24 @@ COMBINED SUMMARY:`,
// Collect tool calls from steps for persistence
result.steps?.forEach((step: any, idx: number) => {
step.toolResults?.forEach((tr: any) => {
const toolOutput = (tr as any).output ?? (tr as any).result;
const toolFailed = isToolResultFailure(toolOutput);
collectedToolCalls.push({
toolName: tr.toolName,
toolCallId: tr.toolCallId,
input: (tr as any).input ?? (tr as any).args,
output: (tr as any).output ?? (tr as any).result,
output: toolOutput,
stepNumber: idx,
status: 'done',
status: toolFailed ? 'error' : 'done',
});
collectedParts.push({
type: 'tool-call',
toolCallId: tr.toolCallId,
toolName: tr.toolName,
args: (tr as any).input ?? (tr as any).args,
result: (tr as any).output ?? (tr as any).result,
status: 'done',
result: toolOutput,
error: toolFailed ? getToolResultError(toolOutput) : undefined,
status: toolFailed ? 'error' : 'done',
});
});
});
Expand Down Expand Up @@ -1383,7 +1395,10 @@ COMBINED SUMMARY:`,
status: tc.status === 'done' ? 'completed' : 'failed',
startedAt: new Date().toISOString(),
completedAt: new Date().toISOString(),
errorMessage: null,
errorMessage:
tc.status === 'error'
? getToolResultError(tc.output) || 'Tool execution failed'
: null,
}));

await MainDatabaseService.addMessageWithContext(
Expand Down
3 changes: 3 additions & 0 deletions src/main/services/ai/agents/projectAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ ${skills ?? ''}
- Before acting, identify how success will be checked.
- Use the smallest reliable verification available, such as connection tests, \`dbt debug\`, dbt logs, file readback, command output, or explicit user confirmation.
- Do not claim success until the relevant outcome has been verified.
- A dbt tool result with \`ok: false\`, \`success: false\`, a nonzero \`exitCode\`, or any models reported as errors is a failed run, even if other models succeeded. Never summarize a partial dbt run as successful.
- For dbt commands, report the processed, successful, and failed counts from the actual execution summary. Claim full success only when the exit code is zero and the error count is zero.
- After creating or changing an incremental model, a successful first build is not sufficient verification. Run it a second time to exercise the \`is_incremental()\` branch, and only claim success when that incremental run also passes.
- If verification fails, explain the failure clearly and stop, retry with evidence, or ask the user for clarification.

## File Ownership Rules
Expand Down
Loading
Loading