snapkitty
hardware
erlang
File size: 3,869 Bytes
b8a03fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import { existsSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { app, BrowserWindow, ipcMain, Menu, shell } from 'electron';

import { BobChatBroker, userChatMessage } from './backend/bob.js';
import { ModelProviderStore } from './backend/providers.js';
import { DesktopCommandBroker } from './backend/sandbox.js';
import { SovereignToolBroker } from './backend/tools.js';
import {
  getWorkspaceOverview,
  listWorkspaceEntries,
  readWorkspaceTextFile,
  saveWorkspaceTextFile,
} from './backend/workspace.js';
import type { BobChatRequest, DesktopCommandRequest, ModelProviderUpdate } from './shared.js';

const moduleDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = moduleDir.replaceAll('\\', '/').endsWith('/dist/desktop')
  ? resolve(moduleDir, '..', '..')
  : resolve(moduleDir, '..');
const preloadPath = resolve(moduleDir, 'preload.js');
const builtRendererPath = resolve(repoRoot, '_site', 'desktop', 'index.html');
const sourceRendererPath = resolve(repoRoot, 'desktop', 'index.html');

let commands: DesktopCommandBroker;
let providers: ModelProviderStore;
let tools: SovereignToolBroker;
let chat: BobChatBroker;

let mainWindow: BrowserWindow | null = null;

async function createWindow(): Promise<void> {
  mainWindow = new BrowserWindow({
    width: 1440,
    height: 960,
    minWidth: 1100,
    minHeight: 720,
    title: 'BOB Desktop IDE',
    backgroundColor: '#111318',
    webPreferences: {
      preload: preloadPath,
      contextIsolation: true,
      nodeIntegration: false,
      sandbox: true,
      webSecurity: true,
    },
  });

  Menu.setApplicationMenu(null);

  const devUrl = process.env.SHADOW_DESKTOP_DEV_URL;
  if (devUrl) {
    await mainWindow.loadURL(devUrl);
  } else {
    const rendererPath = existsSync(builtRendererPath) ? builtRendererPath : sourceRendererPath;
    await mainWindow.loadURL(pathToFileURL(rendererPath).toString());
  }

  mainWindow.on('closed', () => {
    mainWindow = null;
  });
}

function registerIpc(): void {
  commands = new DesktopCommandBroker(repoRoot);
  providers = ModelProviderStore.fromUserData(app.getPath('userData'));
  tools = new SovereignToolBroker(repoRoot, commands);
  chat = new BobChatBroker(providers, tools);

  ipcMain.handle('desktop:workspace', () => getWorkspaceOverview(repoRoot));
  ipcMain.handle('desktop:list', (_event, path?: string) => listWorkspaceEntries(repoRoot, path));
  ipcMain.handle('desktop:read-file', (_event, path: string) => readWorkspaceTextFile(repoRoot, path));
  ipcMain.handle('desktop:save-file', (_event, path: string, content: string) => (
    saveWorkspaceTextFile(repoRoot, path, content)
  ));
  ipcMain.handle('desktop:list-model-providers', () => providers.list());
  ipcMain.handle('desktop:save-model-provider', (_event, update: ModelProviderUpdate) => (
    providers.save(update)
  ));
  ipcMain.handle('desktop:list-tools', () => tools.list());
  ipcMain.handle('desktop:bob-chat', (_event, request: BobChatRequest) => {
    userChatMessage(request.prompt);
    return chat.respond(request);
  });
  ipcMain.handle('desktop:run-command', async (_event, request: DesktopCommandRequest) => (
    commands.run(request, chunk => mainWindow?.webContents.send('desktop:command-chunk', chunk))
  ));
}

app.on('web-contents-created', (_event, contents) => {
  contents.setWindowOpenHandler(({ url }) => {
    void shell.openExternal(url);
    return { action: 'deny' };
  });

  contents.on('will-navigate', event => {
    event.preventDefault();
  });
});

app.whenReady().then(async () => {
  registerIpc();
  await createWindow();

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) void createWindow();
  });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});