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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,7 @@ apps/desktop/resources/nodejs/
# Test local agent Chrome profile
.accomplish-test-local-agent-chrome/
.worktrees

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove duplicate .worktrees entry.

Line 77 duplicates the .worktrees/ pattern already present on line 69. In .gitignore, both .worktrees and .worktrees/ are functionally equivalent for ignoring directories.

🧹 Proposed fix to remove duplicate
-.worktrees
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 77, Remove the duplicate .worktrees ignore entry from
.gitignore by deleting either the `.worktrees` or `.worktrees/` line so only one
remains; locate the redundant pattern (the duplicate `.worktrees`/`.worktrees/`
entries) and remove the second occurrence to keep the file clean and avoid
duplicate patterns.

# MCP tools npm lockfiles (generated at runtime by postinstall.cjs, not committed)
packages/agent-core/mcp-tools/*/package-lock.json
packages/agent-core/mcp-tools/package-lock.json
28 changes: 28 additions & 0 deletions apps/desktop/__tests__/unit/main/ipc/handlers.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,25 @@ vi.mock('@main/opencode/auth', () => ({
loginOpenAiWithChatGpt: vi.fn(() => Promise.resolve({ openedUrl: undefined })),
}));

// Mock HuggingFace Local provider - used by handlers.ts for local inference
vi.mock('@main/providers/huggingface-local', () => ({
startHuggingFaceServer: vi.fn(() => Promise.resolve({ success: true, port: 8080 })),
stopHuggingFaceServer: vi.fn(() => Promise.resolve()),
getHuggingFaceServerStatus: vi.fn(() => ({
running: false,
port: null,
loadedModel: null,
isLoading: false,
})),
testHuggingFaceConnection: vi.fn(() =>
Promise.resolve({ success: false, error: 'Server is not running' }),
),
downloadModel: vi.fn(() => Promise.resolve({ success: true })),
listCachedModels: vi.fn(() => []),
deleteModel: vi.fn(() => Promise.resolve({ success: true })),
SUGGESTED_MODELS: [],
}));

// Mock task history (stored in test state)
const mockTasks: Array<{
id: string;
Expand Down Expand Up @@ -513,6 +532,15 @@ describe('IPC Handlers Integration', () => {
// Shell handler
expect(handlers.has('shell:open-external')).toBe(true);

// HuggingFace Local handlers
expect(handlers.has('huggingface-local:start-server')).toBe(true);
expect(handlers.has('huggingface-local:stop-server')).toBe(true);
expect(handlers.has('huggingface-local:server-status')).toBe(true);
expect(handlers.has('huggingface-local:test-connection')).toBe(true);
expect(handlers.has('huggingface-local:download-model')).toBe(true);
expect(handlers.has('huggingface-local:list-models')).toBe(true);
expect(handlers.has('huggingface-local:delete-model')).toBe(true);

// Log handler
expect(handlers.has('log:event')).toBe(true);
});
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@aws-sdk/client-bedrock": "^3.971.0",
"@aws-sdk/credential-providers": "^3.971.0",
"@azure/identity": "^4.13.0",
"@huggingface/transformers": "^3.8.1",
"better-sqlite3": "catalog:",
"dotenv": "^17.2.3",
"electron-store": "^8.2.0",
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/public/assets/ai-logos/huggingface.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { getApiKey, clearSecureStorage } from './store/secureStorage';
import { initializeLogCollector, shutdownLogCollector, getLogCollector } from './logging';
import { skillsManager } from './skills';
import { startHuggingFaceServer, stopHuggingFaceServer } from './providers/huggingface-local';

if (process.argv.includes('--e2e-skip-auth')) {
(global as Record<string, unknown>).E2E_SKIP_AUTH = true;
Expand Down Expand Up @@ -288,6 +289,17 @@ if (!gotTheLock) {
}
}
}

// Auto-start HuggingFace local server if enabled
const hfConfig = storage.getHuggingFaceLocalConfig();
if (hfConfig?.enabled && hfConfig.selectedModelId) {
console.log(
`[Main] Auto-starting HuggingFace server for model: ${hfConfig.selectedModelId}`,
);
startHuggingFaceServer(hfConfig.selectedModelId).catch((err: unknown) => {
console.error('[Main] Failed to auto-start HuggingFace local server:', err);
});
}
} catch (err) {
console.error('[Main] Provider validation failed:', err);
}
Expand Down Expand Up @@ -343,6 +355,10 @@ app.on('before-quit', () => {
oauthBrowserFlow.dispose();
closeStorage();
shutdownLogCollector();
// Stop the HF inference server so its port is released before process exit
stopHuggingFaceServer().catch((err: unknown) => {
console.warn('[Main] Failed to stop HuggingFace server on quit:', err);
});
});

if (process.platform === 'win32' && !app.isPackaged) {
Expand Down
98 changes: 94 additions & 4 deletions apps/desktop/src/main/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import type {
AzureFoundryConfig,
LiteLLMConfig,
LMStudioConfig,
HuggingFaceLocalConfig,
} from '@accomplish_ai/agent-core';
import {
DEFAULT_PROVIDERS,
Expand All @@ -99,6 +100,16 @@ import {
} from '../test-utils/mock-task-flow';
import { skillsManager } from '../skills';
import { registerVertexHandlers } from '../providers';
import {
startHuggingFaceServer,
stopHuggingFaceServer,
getHuggingFaceServerStatus,
testHuggingFaceConnection,
downloadModel as hfDownloadModel,
listCachedModels as hfListCachedModels,
deleteModel as hfDeleteModel,
SUGGESTED_MODELS as HF_SUGGESTED_MODELS,
} from '../providers/huggingface-local';

const API_KEY_VALIDATION_TIMEOUT_MS = 15000;
const MAX_ATTACHMENT_FILE_SIZE = 10 * 1024 * 1024; // 10MB
Expand Down Expand Up @@ -1392,10 +1403,6 @@ export function registerIPCHandlers(): void {
return skillsManager.getContent(id);
});

handle('skills:get-user-skills-path', async () => {
return skillsManager.getUserSkillsPath();
});

handle('skills:pick-file', async () => {
const mainWindow = BrowserWindow.getAllWindows()[0];
const result = await dialog.showOpenDialog(mainWindow, {
Expand Down Expand Up @@ -1437,6 +1444,10 @@ export function registerIPCHandlers(): void {
shell.showItemInFolder(filePath);
});

handle('skills:get-user-skills-path', async () => {
return skillsManager.getUserSkillsPath();
});

// ── MCP Connectors ──────────────────────────────────────────────────

handle('connectors:list', async () => {
Expand Down Expand Up @@ -1579,6 +1590,85 @@ export function registerIPCHandlers(): void {
storage.deleteConnectorTokens(connectorId);
storage.setConnectorStatus(connectorId, 'disconnected');
});

// ── HuggingFace Local Provider ──────────────────────────────────────

handle('huggingface-local:start-server', async (_event: IpcMainInvokeEvent, modelId: string) => {
if (typeof modelId !== 'string' || !modelId.trim()) {
return { success: false, error: 'Invalid model ID' };
}
return startHuggingFaceServer(modelId.trim());
});

handle('huggingface-local:stop-server', async () => {
await stopHuggingFaceServer();
return { success: true };
});

handle('huggingface-local:server-status', async () => {
return getHuggingFaceServerStatus();
});

handle('huggingface-local:test-connection', async () => {
return testHuggingFaceConnection();
});

handle('huggingface-local:download-model', async (event: IpcMainInvokeEvent, modelId: string) => {
if (typeof modelId !== 'string' || !modelId.trim()) {
return { success: false, error: 'Invalid model ID' };
}
return hfDownloadModel(modelId.trim(), (progress) => {
try {
event.sender.send('huggingface-local:download-progress', progress);
} catch {
// Window may have been closed
}
});
});

handle('huggingface-local:list-models', async () => {
const cached = hfListCachedModels();
return { cached, suggested: HF_SUGGESTED_MODELS };
});

handle('huggingface-local:delete-model', async (_event: IpcMainInvokeEvent, modelId: string) => {
if (typeof modelId !== 'string' || !modelId.trim()) {
return { success: false, error: 'Invalid model ID' };
}
return hfDeleteModel(modelId.trim());
});

handle('huggingface-local:get-config', async () => {
return storage.getHuggingFaceLocalConfig();
});

handle(
'huggingface-local:set-config',
async (_event: IpcMainInvokeEvent, config: HuggingFaceLocalConfig | null) => {
if (config !== null) {
const validQuantizations = ['q4', 'fp32'];
const validDevicePreferences = ['auto', 'cpu', 'cuda', 'webgpu'];
if (
typeof config !== 'object' ||
(config.selectedModelId !== null && typeof config.selectedModelId !== 'string') ||
(config.serverPort !== null &&
!(
Number.isInteger(config.serverPort) &&
isFinite(config.serverPort) &&
config.serverPort >= 1 &&
config.serverPort <= 65535
)) ||
typeof config.enabled !== 'boolean' ||
(config.quantization !== null && !validQuantizations.includes(config.quantization)) ||
(config.devicePreference !== null &&
!validDevicePreferences.includes(config.devicePreference))
) {
throw new Error('Invalid HuggingFace config: unexpected field types');
}
}
storage.setHuggingFaceLocalConfig(config);
},
);
}

// In-memory store for pending OAuth flows (keyed by state parameter)
Expand Down
35 changes: 6 additions & 29 deletions apps/desktop/src/main/ipc/task-callbacks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { BrowserWindow } from 'electron';
import type { TaskMessage, TaskResult, TaskStatus, TodoItem } from '@accomplish_ai/agent-core';
import { mapResultToStatus } from '@accomplish_ai/agent-core';
import { getTaskManager, recoverDevBrowserServer } from '../opencode';
import { getTaskManager } from '../opencode';
import type { TaskCallbacks } from '../opencode';
import { getStorage } from '../store/storage';

Expand Down Expand Up @@ -184,34 +184,11 @@ export function createTaskCallbacks(options: TaskCallbacksOptions): TaskCallback
)}s). Reconnecting browser...`;

console.warn(`[TaskCallbacks] ${reason}`);

void recoverDevBrowserServer(
{
onProgress: (progress) => {
forwardToRenderer('task:progress', {
taskId,
...progress,
});
},
},
{ reason },
)
.catch((error) => {
const errorMessage = error instanceof Error ? error.message : String(error);
console.warn('[TaskCallbacks] Browser recovery failed:', errorMessage);
if (storage.getDebugMode()) {
forwardToRenderer('debug:log', {
taskId,
timestamp: new Date().toISOString(),
type: 'warning',
message: `Browser recovery failed: ${errorMessage}`,
});
}
})
.finally(() => {
browserRecoveryInFlight = false;
resetBrowserFailureState();
});
console.warn(
`[TaskCallbacks] recoverDevBrowserServer is currently unavailable. Please restart the browser.`,
);
browserRecoveryInFlight = false;
resetBrowserFailureState();
},
};
}
Loading
Loading