loadtoagent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.ko.md +175 -0
  2. package/README.md +175 -0
  3. package/README.zh-CN.md +175 -0
  4. package/bin/loadtoagent.js +171 -0
  5. package/docs/ARCHITECTURE.md +30 -0
  6. package/docs/PROVIDER-CONTRACTS.md +58 -0
  7. package/docs/assets/loadtoagent-dashboard.png +0 -0
  8. package/docs/assets/loadtoagent-demo.gif +0 -0
  9. package/main.js +493 -0
  10. package/package.json +133 -0
  11. package/preload.js +75 -0
  12. package/renderer/app-agent-actions.js +285 -0
  13. package/renderer/app-bootstrap.js +95 -0
  14. package/renderer/app-dashboard.js +361 -0
  15. package/renderer/app-drawer-content.js +203 -0
  16. package/renderer/app-drawer-data.js +41 -0
  17. package/renderer/app-drawer.js +183 -0
  18. package/renderer/app-events-dialogs.js +169 -0
  19. package/renderer/app-events-filters.js +74 -0
  20. package/renderer/app-events-navigation.js +96 -0
  21. package/renderer/app-events-sessions.js +195 -0
  22. package/renderer/app-events.js +16 -0
  23. package/renderer/app-graph-layout.js +71 -0
  24. package/renderer/app-graph-model.js +107 -0
  25. package/renderer/app-graph-orchestration.js +76 -0
  26. package/renderer/app-graph-view.js +544 -0
  27. package/renderer/app-run-modal.js +221 -0
  28. package/renderer/app-session-render.js +243 -0
  29. package/renderer/app-tmux-render.js +247 -0
  30. package/renderer/app.js +608 -0
  31. package/renderer/fonts/geist-ext.woff2 +0 -0
  32. package/renderer/fonts/geist.woff2 +0 -0
  33. package/renderer/i18n-messages.js +355 -0
  34. package/renderer/i18n.js +122 -0
  35. package/renderer/index.html +386 -0
  36. package/renderer/shared.js +26 -0
  37. package/renderer/styles-agent-map.css +401 -0
  38. package/renderer/styles-cards.css +600 -0
  39. package/renderer/styles-collaboration.css +534 -0
  40. package/renderer/styles-components.css +1293 -0
  41. package/renderer/styles-onboarding.css +344 -0
  42. package/renderer/styles-overlays.css +284 -0
  43. package/renderer/styles-product.css +686 -0
  44. package/renderer/styles-responsive-product.css +689 -0
  45. package/renderer/styles-responsive-runtime.css +718 -0
  46. package/renderer/styles-responsive-shell.css +533 -0
  47. package/renderer/styles-responsive-workflows.css +778 -0
  48. package/renderer/styles-run-composer.css +695 -0
  49. package/renderer/styles-settings.css +606 -0
  50. package/renderer/styles-terminal.css +1241 -0
  51. package/renderer/styles-tmux.css +1162 -0
  52. package/renderer/styles-workflow-map.css +513 -0
  53. package/renderer/styles-workflows.css +1217 -0
  54. package/renderer/styles.css +486 -0
  55. package/renderer/terminal-agent.js +152 -0
  56. package/renderer/terminal-events.js +226 -0
  57. package/renderer/terminal-workbench.js +464 -0
  58. package/renderer/terminal.js +497 -0
  59. package/src/agentMonitor/claudeParser.js +197 -0
  60. package/src/agentMonitor/codexCollaboration.js +95 -0
  61. package/src/agentMonitor/codexParser.js +447 -0
  62. package/src/agentMonitor/genericParser.js +244 -0
  63. package/src/agentMonitor/hierarchy.js +248 -0
  64. package/src/agentMonitor/sessionFiles.js +86 -0
  65. package/src/agentMonitor.js +591 -0
  66. package/src/agentRunner.js +465 -0
  67. package/src/bridgeServer.js +246 -0
  68. package/src/contracts.js +71 -0
  69. package/src/diagnostics.js +23 -0
  70. package/src/ipc/registerAgentIpc.js +12 -0
  71. package/src/ipc/registerAppIpc.js +20 -0
  72. package/src/ipc/registerTerminalIpc.js +50 -0
  73. package/src/ipc/registerTmuxIpc.js +30 -0
  74. package/src/ipc/registerWorkspaceIpc.js +14 -0
  75. package/src/monitorWorker.js +273 -0
  76. package/src/processMonitor.js +394 -0
  77. package/src/providerRegistry.js +120 -0
  78. package/src/terminalManager.js +438 -0
  79. package/src/tmuxController.js +183 -0
  80. package/src/tmuxMonitor.js +432 -0
  81. package/src/updateManager.js +339 -0
  82. package/src/workspaceStore.js +77 -0
package/main.js ADDED
@@ -0,0 +1,493 @@
1
+ 'use strict';
2
+
3
+ const { app, BrowserWindow, ipcMain, dialog, shell, clipboard, Tray, Menu, net } = require('electron');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const { pathToFileURL } = require('url');
8
+ const { Worker } = require('worker_threads');
9
+ const { execFileSync } = require('child_process');
10
+ const { AgentRunner, probeProviders } = require('./src/agentRunner');
11
+ const { providerList, blankUsage } = require('./src/providerRegistry');
12
+ const { TerminalManager } = require('./src/terminalManager');
13
+ const { TmuxController } = require('./src/tmuxController');
14
+ const { normalizeWslList } = require('./src/tmuxMonitor');
15
+ const { BridgeServer } = require('./src/bridgeServer');
16
+ const { UpdateManager } = require('./src/updateManager');
17
+ const { readWorkspaces, removeWorkspace, writeWorkspaces } = require('./src/workspaceStore');
18
+ const { registerAppIpc } = require('./src/ipc/registerAppIpc');
19
+ const { registerAgentIpc } = require('./src/ipc/registerAgentIpc');
20
+ const { registerTerminalIpc } = require('./src/ipc/registerTerminalIpc');
21
+ const { registerTmuxIpc } = require('./src/ipc/registerTmuxIpc');
22
+ const { registerWorkspaceIpc } = require('./src/ipc/registerWorkspaceIpc');
23
+ const { reportRecoverableError } = require('./src/diagnostics');
24
+
25
+ const demoCapture = process.env.LOADTOAGENT_DEMO_CAPTURE === '1';
26
+ let mainWindow = null;
27
+ let monitorWorker = null;
28
+ let runner = null;
29
+ let terminalManager = null;
30
+ let bridgeServer = null;
31
+ let bridgeLauncher = null;
32
+ let backgroundTray = null;
33
+ let updateManager = null;
34
+ let isQuitting = false;
35
+ let appLocale = 'ko';
36
+ const tmuxController = new TmuxController({ platform: process.platform });
37
+ let availability = {};
38
+ let detailRequestId = 0;
39
+ const pendingDetails = new Map();
40
+ const MAIN_COPY = {
41
+ ko: {
42
+ trayTooltip: 'LoadToAgent · 백그라운드 AI 세션 {count}개',
43
+ trayOpen: 'LoadToAgent 열기',
44
+ traySessions: '백그라운드 AI 세션 {count}개 유지 중',
45
+ trayQuit: '프로그램 끝내기 · AI 세션도 종료',
46
+ addWorkspaces: 'AI 작업 폴더 선택',
47
+ pickWorkspace: '작업 폴더 선택',
48
+ },
49
+ en: {
50
+ trayTooltip: 'LoadToAgent · {count} background AI sessions',
51
+ trayOpen: 'Open LoadToAgent',
52
+ traySessions: '{count} background AI sessions active',
53
+ trayQuit: 'Quit app · End AI sessions too',
54
+ addWorkspaces: 'Choose AI workspaces',
55
+ pickWorkspace: 'Choose workspace',
56
+ },
57
+ 'zh-CN': {
58
+ trayTooltip: 'LoadToAgent · {count} 个后台 AI 会话',
59
+ trayOpen: '打开 LoadToAgent',
60
+ traySessions: '正在保持 {count} 个后台 AI 会话',
61
+ trayQuit: '退出应用 · 同时结束 AI 会话',
62
+ addWorkspaces: '选择 AI 工作文件夹',
63
+ pickWorkspace: '选择工作文件夹',
64
+ },
65
+ };
66
+ let lastSnapshot = {
67
+ generatedAt: new Date().toISOString(),
68
+ sessions: [],
69
+ tmux: { generatedAt: new Date().toISOString(), available: false, status: '확인 중', distros: [], summary: { distros: 0, sessions: 0, windows: 0, panes: 0, aiPanes: 0, linked: 0 } },
70
+ summary: {
71
+ providers: providerList().map(provider => ({ ...provider, installed: false, sessions: 0, active: 0, waiting: 0, subagents: 0, usage: blankUsage() })),
72
+ totals: { sessions: 0, active: 0, waiting: 0, subagents: 0, usage: blankUsage() },
73
+ },
74
+ };
75
+
76
+ const isolatedTestInstance = process.env.LOADTOAGENT_TEST_INSTANCE === '1';
77
+ const bridgeHome = process.env.LOADTOAGENT_BRIDGE_HOME || os.homedir();
78
+ const singleInstance = isolatedTestInstance || app.requestSingleInstanceLock();
79
+ if (!singleInstance) app.quit();
80
+ else app.on('second-instance', () => {
81
+ if (!mainWindow || mainWindow.isDestroyed()) return;
82
+ if (mainWindow.isMinimized()) mainWindow.restore();
83
+ mainWindow.show();
84
+ mainWindow.focus();
85
+ });
86
+
87
+ function userFile(name) {
88
+ return path.join(app.getPath('userData'), name);
89
+ }
90
+
91
+ function mainText(key, values = {}) {
92
+ const source = MAIN_COPY[appLocale]?.[key] || MAIN_COPY.ko[key] || key;
93
+ return Object.entries(values).reduce((text, [name, value]) => text.replaceAll(`{${name}}`, String(value)), source);
94
+ }
95
+
96
+ function shellQuote(value) {
97
+ return `'${String(value || '').replace(/'/g, `'"'"'`)}'`;
98
+ }
99
+
100
+ function installBridgeLauncher(home = bridgeHome) {
101
+ const directory = path.join(home, '.loadtoagent', 'bin');
102
+ fs.mkdirSync(directory, { recursive: true });
103
+ const script = app.isPackaged
104
+ ? path.join(process.resourcesPath, 'app.asar.unpacked', 'bin', 'loadtoagent.js')
105
+ : path.join(__dirname, 'bin', 'loadtoagent.js');
106
+ if (process.platform === 'win32') {
107
+ const launcher = path.join(directory, 'loadtoagent.cmd');
108
+ const content = `@echo off\r\nset "ELECTRON_RUN_AS_NODE=1"\r\n"${process.execPath}" "${script}" %*\r\n`;
109
+ fs.writeFileSync(launcher, content, 'utf8');
110
+ return { path: launcher, directory, commandPrefix: `& "${launcher}"`, simpleCommand: 'loadtoagent' };
111
+ }
112
+ const launcher = path.join(directory, 'loadtoagent');
113
+ const content = `#!/bin/sh\nELECTRON_RUN_AS_NODE=1 exec ${shellQuote(process.execPath)} ${shellQuote(script)} "$@"\n`;
114
+ fs.writeFileSync(launcher, content, { encoding: 'utf8', mode: 0o755 });
115
+ fs.chmodSync(launcher, 0o755);
116
+ return { path: launcher, directory, commandPrefix: shellQuote(launcher), simpleCommand: 'loadtoagent' };
117
+ }
118
+
119
+ function listWorkspaces() {
120
+ return readWorkspaces(userFile('workspaces.json'));
121
+ }
122
+
123
+ function saveWorkspaces(items) {
124
+ return writeWorkspaces(userFile('workspaces.json'), items);
125
+ }
126
+
127
+ function listWslDistros() {
128
+ if (process.platform === 'darwin') return ['macOS'];
129
+ if (process.platform !== 'win32') return ['로컬'];
130
+ try {
131
+ return normalizeWslList(execFileSync('wsl.exe', ['--list', '--quiet'], {
132
+ windowsHide: true,
133
+ timeout: 5_000,
134
+ maxBuffer: 256 * 1024,
135
+ }));
136
+ } catch (error) {
137
+ reportRecoverableError('wsl-distro-list', error);
138
+ return [];
139
+ }
140
+ }
141
+
142
+ function hydratePlatformPath() {
143
+ if (process.platform !== 'darwin') return;
144
+ const additions = ['/opt/homebrew/bin', '/usr/local/bin', path.join(os.homedir(), '.local', 'bin')];
145
+ try {
146
+ const shellPath = process.env.SHELL || '/bin/zsh';
147
+ const loginPath = execFileSync(shellPath, ['-lic', 'printf %s "$PATH"'], { encoding: 'utf8', timeout: 5_000 }).trim();
148
+ additions.unshift(...loginPath.split(path.delimiter));
149
+ } catch (error) {
150
+ reportRecoverableError('platform-path', error);
151
+ }
152
+ process.env.PATH = [...new Set([...additions, ...String(process.env.PATH || '').split(path.delimiter)].filter(Boolean))].join(path.delimiter);
153
+ }
154
+
155
+ function createWindow() {
156
+ mainWindow = new BrowserWindow({
157
+ width: 1600,
158
+ height: 980,
159
+ minWidth: 360,
160
+ minHeight: 520,
161
+ title: 'LoadToAgent · AI Agent Observatory',
162
+ backgroundColor: '#080b12',
163
+ show: false,
164
+ webPreferences: {
165
+ preload: path.join(__dirname, 'preload.js'),
166
+ contextIsolation: true,
167
+ nodeIntegration: false,
168
+ sandbox: false,
169
+ },
170
+ });
171
+ mainWindow.setMenuBarVisibility(false);
172
+ mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
173
+ mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
174
+ const allowedUrl = pathToFileURL(path.join(__dirname, 'renderer', 'index.html')).href;
175
+ mainWindow.webContents.on('will-navigate', (event, url) => { if (url !== allowedUrl) event.preventDefault(); });
176
+ mainWindow.once('ready-to-show', () => mainWindow && mainWindow.show());
177
+ mainWindow.on('close', event => {
178
+ if (isQuitting || !backgroundAgentSessions().length) return;
179
+ event.preventDefault();
180
+ mainWindow.hide();
181
+ ensureBackgroundTray();
182
+ });
183
+ mainWindow.on('closed', () => { mainWindow = null; });
184
+ }
185
+
186
+ function backgroundAgentSessions() {
187
+ if (!terminalManager) return [];
188
+ return terminalManager.list().filter(session => session.type === 'agent' && (session.status === 'running' || session.status === 'starting'));
189
+ }
190
+
191
+ function showMainWindow() {
192
+ if (!mainWindow || mainWindow.isDestroyed()) createWindow();
193
+ if (mainWindow.isMinimized()) mainWindow.restore();
194
+ mainWindow.show();
195
+ mainWindow.focus();
196
+ }
197
+
198
+ function updateBackgroundTrayMenu() {
199
+ if (!backgroundTray) return;
200
+ const count = backgroundAgentSessions().length;
201
+ backgroundTray.setToolTip(mainText('trayTooltip', { count }));
202
+ backgroundTray.setContextMenu(Menu.buildFromTemplate([
203
+ { label: mainText('trayOpen'), click: showMainWindow },
204
+ { label: mainText('traySessions', { count }), enabled: false },
205
+ { type: 'separator' },
206
+ { label: mainText('trayQuit'), click: () => { isQuitting = true; app.quit(); } },
207
+ ]));
208
+ }
209
+
210
+ async function ensureBackgroundTray() {
211
+ if (backgroundTray || isQuitting) return backgroundTray;
212
+ try {
213
+ const icon = await app.getFileIcon(process.execPath, { size: 'small' });
214
+ if (isQuitting || backgroundTray) return backgroundTray;
215
+ backgroundTray = new Tray(icon);
216
+ backgroundTray.on('click', showMainWindow);
217
+ backgroundTray.on('double-click', showMainWindow);
218
+ updateBackgroundTrayMenu();
219
+ } catch (error) {
220
+ reportRecoverableError('background-tray', error);
221
+ }
222
+ return backgroundTray;
223
+ }
224
+
225
+ function trustedSender(event) {
226
+ if (!mainWindow || mainWindow.isDestroyed() || !event || !event.sender || event.sender.id !== mainWindow.webContents.id) return false;
227
+ const allowedUrl = pathToFileURL(path.join(__dirname, 'renderer', 'index.html')).href;
228
+ const senderUrl = event.senderFrame && event.senderFrame.url || event.sender.getURL();
229
+ return senderUrl === allowedUrl;
230
+ }
231
+
232
+ function requireTrustedSender(event) {
233
+ if (!trustedSender(event)) throw new Error('허용되지 않은 터미널 요청입니다.');
234
+ }
235
+
236
+ function handleTrusted(channel, handler) {
237
+ ipcMain.handle(channel, (event, ...args) => {
238
+ requireTrustedSender(event);
239
+ return handler(...args);
240
+ });
241
+ }
242
+
243
+ function sendTerminal(channel, payload) {
244
+ if (!mainWindow || mainWindow.isDestroyed()) return;
245
+ try { mainWindow.webContents.send(channel, payload); } catch (error) { reportRecoverableError(`ipc-send:${channel}`, error); }
246
+ }
247
+
248
+ function refreshMonitor() {
249
+ if (monitorWorker) monitorWorker.postMessage({ type: 'scan' });
250
+ }
251
+
252
+ function sendSnapshot(snapshot) {
253
+ if (!mainWindow || mainWindow.isDestroyed()) return;
254
+ try { mainWindow.webContents.send('agents:snapshot', snapshot); } catch (error) { reportRecoverableError('ipc-send:agents:snapshot', error); }
255
+ }
256
+
257
+ function sendUpdateState(update) {
258
+ if (!mainWindow || mainWindow.isDestroyed()) return;
259
+ try { mainWindow.webContents.send('app:update-state', update); } catch (error) { reportRecoverableError('ipc-send:app:update-state', error); }
260
+ }
261
+
262
+ function installationType() {
263
+ if (app.isPackaged) return 'desktop';
264
+ return fs.existsSync(path.join(__dirname, '.git')) ? 'source' : 'npm';
265
+ }
266
+
267
+ function setupRuntime() {
268
+ const runsDir = userFile('agent-runs');
269
+ runner = new AgentRunner({ runsDir });
270
+ terminalManager = new TerminalManager();
271
+ updateManager = new UpdateManager({
272
+ currentVersion: app.getVersion(),
273
+ platform: process.platform,
274
+ arch: process.arch,
275
+ installType: installationType(),
276
+ fetch: (...args) => net.fetch(...args),
277
+ shell,
278
+ downloadsDir: path.join(app.getPath('userData'), 'updates'),
279
+ });
280
+ updateManager.on('state', sendUpdateState);
281
+ updateManager.check().catch(error => reportRecoverableError('startup-update-check', error));
282
+ if (demoCapture) {
283
+ availability = Object.fromEntries(providerList().map(provider => [provider.id, true]));
284
+ return;
285
+ }
286
+ try {
287
+ bridgeLauncher = installBridgeLauncher(bridgeHome);
288
+ } catch (error) {
289
+ bridgeLauncher = null;
290
+ reportRecoverableError('bridge-launcher-install', error);
291
+ }
292
+ terminalManager.on('data', payload => sendTerminal('terminals:data', payload));
293
+ terminalManager.on('state', payload => {
294
+ sendTerminal('terminals:state', payload);
295
+ updateBackgroundTrayMenu();
296
+ if (monitorWorker) monitorWorker.postMessage({ type: 'bridge-presence', bridges: bridgePresence() });
297
+ });
298
+ bridgeServer = new BridgeServer({ terminalManager, home: bridgeHome, platform: process.platform });
299
+ bridgeServer.start().catch(error => sendTerminal('terminals:error', { id: '', message: `외부 터미널 브리지를 열지 못했습니다: ${error.message}` }));
300
+ availability = probeProviders();
301
+ monitorWorker = new Worker(path.join(__dirname, 'src', 'monitorWorker.js'), {
302
+ workerData: { runsDir, home: os.homedir(), intervalMs: 1200, availability },
303
+ });
304
+ monitorWorker.postMessage({ type: 'bridge-presence', bridges: bridgePresence() });
305
+ monitorWorker.on('message', message => {
306
+ if (message && message.type === 'snapshot') {
307
+ lastSnapshot = message.snapshot;
308
+ sendSnapshot(lastSnapshot);
309
+ }
310
+ if (message && message.type === 'detail-result') {
311
+ const pending = pendingDetails.get(message.requestId);
312
+ if (pending) {
313
+ pendingDetails.delete(message.requestId);
314
+ pending.resolve(message.session);
315
+ }
316
+ }
317
+ });
318
+ monitorWorker.on('error', error => {
319
+ if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('agents:monitor-error', error.message);
320
+ });
321
+ runner.on('changed', () => monitorWorker && monitorWorker.postMessage({ type: 'scan' }));
322
+ }
323
+
324
+ function bridgePresence() {
325
+ if (!terminalManager) return [];
326
+ const environment = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'macos' : 'linux');
327
+ return terminalManager.list()
328
+ .filter(session => session.type === 'agent' && session.status === 'running')
329
+ .map(session => ({
330
+ id: session.bridgeId || session.id,
331
+ bridgeId: session.bridgeId || '',
332
+ terminalId: session.id,
333
+ provider: session.provider,
334
+ pid: session.pid,
335
+ cwd: session.cwd,
336
+ startedAt: session.createdAt,
337
+ environment,
338
+ kind: 'bridge',
339
+ label: 'LoadToAgent 외부 터미널 브리지',
340
+ }));
341
+ }
342
+
343
+ /** @returns {import('./src/contracts').BootstrapPayload} */
344
+ function bootstrapState() {
345
+ return {
346
+ providers: providerList(),
347
+ availability,
348
+ workspaces: listWorkspaces(),
349
+ snapshot: lastSnapshot,
350
+ activeRuns: runner ? runner.listActive() : [],
351
+ versions: { app: app.getVersion(), electron: process.versions.electron, node: process.versions.node },
352
+ platform: {
353
+ id: process.platform,
354
+ label: process.platform === 'darwin' ? 'macOS' : (process.platform === 'win32' ? 'Windows' : 'Linux'),
355
+ localShell: process.platform === 'win32' ? 'powershell' : 'shell',
356
+ localShellLabel: process.platform === 'darwin' ? 'Mac 명령창' : (process.platform === 'win32' ? 'Windows 명령창' : 'Linux 명령창'),
357
+ nativeTmux: process.platform !== 'win32',
358
+ },
359
+ bridgeCli: bridgeLauncher,
360
+ update: updateManager ? updateManager.getState() : null,
361
+ };
362
+ }
363
+
364
+ function requestAgentDetail(sessionId) {
365
+ return new Promise(resolve => {
366
+ if (!monitorWorker || String(sessionId || '').length > 500) return resolve(null);
367
+ const requestId = ++detailRequestId;
368
+ const timer = setTimeout(() => {
369
+ if (!pendingDetails.has(requestId)) return;
370
+ pendingDetails.delete(requestId);
371
+ resolve(null);
372
+ }, 4000);
373
+ pendingDetails.set(requestId, {
374
+ resolve: value => {
375
+ clearTimeout(timer);
376
+ resolve(value);
377
+ },
378
+ });
379
+ monitorWorker.postMessage({ type: 'detail', requestId, sessionId: String(sessionId || '') });
380
+ });
381
+ }
382
+
383
+ function registerIpcHandlers() {
384
+ registerAppIpc({
385
+ handleTrusted,
386
+ bootstrap: bootstrapState,
387
+ backgroundState: () => ({
388
+ visible: Boolean(mainWindow && !mainWindow.isDestroyed() && mainWindow.isVisible()),
389
+ backgroundSessions: backgroundAgentSessions().length,
390
+ trayReady: Boolean(backgroundTray),
391
+ }),
392
+ show: () => { showMainWindow(); return { ok: true }; },
393
+ setLocale: locale => {
394
+ appLocale = ['ko', 'en', 'zh-CN'].includes(locale) ? locale : 'ko';
395
+ updateBackgroundTrayMenu();
396
+ return { locale: appLocale };
397
+ },
398
+ updateManager: () => updateManager,
399
+ });
400
+ registerAgentIpc({
401
+ handleTrusted,
402
+ snapshot: () => { refreshMonitor(); return lastSnapshot; },
403
+ requestDetail: requestAgentDetail,
404
+ runner: () => runner,
405
+ probeProviders: () => {
406
+ availability = probeProviders();
407
+ if (monitorWorker) monitorWorker.postMessage({ type: 'availability', availability });
408
+ refreshMonitor();
409
+ return availability;
410
+ },
411
+ });
412
+ registerTerminalIpc({
413
+ ipcMain,
414
+ requireTrustedSender,
415
+ trustedSender,
416
+ manager: () => terminalManager,
417
+ listWslDistros,
418
+ sendError: payload => sendTerminal('terminals:error', payload),
419
+ });
420
+ registerTmuxIpc({ handleTrusted, controller: tmuxController, refresh: refreshMonitor });
421
+ registerWorkspaceIpc({
422
+ handleTrusted,
423
+ list: listWorkspaces,
424
+ add: async () => {
425
+ const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory', 'multiSelections'], title: mainText('addWorkspaces') });
426
+ if (result.canceled) return listWorkspaces();
427
+ return saveWorkspaces([...listWorkspaces(), ...result.filePaths.map(folder => ({ path: folder, name: path.basename(folder) }))]);
428
+ },
429
+ remove: folder => saveWorkspaces(removeWorkspace(listWorkspaces(), folder)),
430
+ pick: async () => {
431
+ const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'], title: mainText('pickWorkspace') });
432
+ return result.canceled ? null : result.filePaths[0];
433
+ },
434
+ openExternal: async target => {
435
+ const value = String(target || '');
436
+ if (!/^https:\/\//i.test(value)) return { ok: false };
437
+ await shell.openExternal(value);
438
+ return { ok: true };
439
+ },
440
+ writeClipboard: value => {
441
+ clipboard.writeText(String(value || '').slice(0, 8_000));
442
+ return { ok: true };
443
+ },
444
+ bridgeCommand: provider => {
445
+ const id = String(provider || '').toLowerCase();
446
+ if (!['claude', 'codex', 'gemini', 'grok'].includes(id)) return { ok: false };
447
+ const prefix = bridgeLauncher && bridgeLauncher.commandPrefix || 'loadtoagent';
448
+ return { ok: true, command: `${prefix} run ${id}`, launcher: bridgeLauncher };
449
+ },
450
+ openOrigin: async session => {
451
+ const provider = String(session && session.provider || '');
452
+ const externalId = String(session && session.externalId || '');
453
+ const clientKind = String(session && session.clientKind || '');
454
+ if (provider === 'codex' && clientKind === 'codex-desktop' && /^[0-9a-f-]{20,80}$/i.test(externalId)) {
455
+ await shell.openExternal(`codex://threads/${encodeURIComponent(externalId)}`);
456
+ return { ok: true };
457
+ }
458
+ if (provider === 'claude' && clientKind === 'claude-desktop') {
459
+ await shell.openExternal('claude://');
460
+ return { ok: true };
461
+ }
462
+ return { ok: false };
463
+ },
464
+ });
465
+ }
466
+
467
+ registerIpcHandlers();
468
+
469
+ app.whenReady().then(() => {
470
+ hydratePlatformPath();
471
+ setupRuntime();
472
+ createWindow();
473
+ app.on('activate', showMainWindow);
474
+ });
475
+
476
+ app.on('window-all-closed', () => {
477
+ if (process.platform !== 'darwin') app.quit();
478
+ });
479
+
480
+ app.on('before-quit', () => {
481
+ isQuitting = true;
482
+ if (bridgeServer) bridgeServer.dispose();
483
+ if (terminalManager) terminalManager.dispose();
484
+ if (monitorWorker) {
485
+ monitorWorker.postMessage({ type: 'stop' });
486
+ monitorWorker.terminate();
487
+ }
488
+ });
489
+
490
+ app.on('will-quit', () => {
491
+ if (backgroundTray) backgroundTray.destroy();
492
+ backgroundTray = null;
493
+ });
package/package.json ADDED
@@ -0,0 +1,133 @@
1
+ {
2
+ "name": "loadtoagent",
3
+ "version": "1.0.0",
4
+ "description": "A local desktop dashboard for Claude, Codex, Gemini, and Grok sessions.",
5
+ "main": "main.js",
6
+ "author": "wincube AX",
7
+ "license": "UNLICENSED",
8
+ "homepage": "https://github.com/minjund/LodeToAgent#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/minjund/LodeToAgent.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/minjund/LodeToAgent/issues"
15
+ },
16
+ "keywords": [
17
+ "ai",
18
+ "agent",
19
+ "claude",
20
+ "codex",
21
+ "gemini",
22
+ "grok",
23
+ "electron",
24
+ "desktop"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "files": [
30
+ "bin",
31
+ "main.js",
32
+ "preload.js",
33
+ "renderer",
34
+ "src",
35
+ "docs",
36
+ "README.md",
37
+ "README.ko.md",
38
+ "README.zh-CN.md"
39
+ ],
40
+ "bin": {
41
+ "loadtoagent": "bin/loadtoagent.js"
42
+ },
43
+ "scripts": {
44
+ "start": "electron .",
45
+ "test": "node scripts/regression-test.js",
46
+ "test:terminal": "electron scripts/terminal-integration-test.js",
47
+ "test:bridge": "electron scripts/bridge-integration-test.js",
48
+ "test:tmux": "node scripts/tmux-integration-test.js",
49
+ "test:visual": "electron scripts/visual-check.js",
50
+ "test:responsive": "electron scripts/responsive-check.js",
51
+ "test:interaction": "electron scripts/interaction-check.js",
52
+ "demo:readme": "electron scripts/readme-demo.js",
53
+ "dist": "electron-builder",
54
+ "dist:win": "electron-builder --win portable",
55
+ "dist:mac": "electron-builder --mac --publish never",
56
+ "pack": "electron-builder --dir",
57
+ "prepublishOnly": "npm test"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public",
61
+ "provenance": true
62
+ },
63
+ "dependencies": {
64
+ "@xterm/addon-fit": "^0.11.0",
65
+ "@xterm/xterm": "^6.0.0",
66
+ "node-pty": "^1.1.0"
67
+ },
68
+ "peerDependencies": {
69
+ "electron": "^31.0.0"
70
+ },
71
+ "devDependencies": {
72
+ "electron": "^31.0.0",
73
+ "electron-builder": "^24.13.3"
74
+ },
75
+ "build": {
76
+ "appId": "com.wincube.loadtoagent",
77
+ "productName": "LoadToAgent",
78
+ "directories": {
79
+ "output": "release"
80
+ },
81
+ "files": [
82
+ "main.js",
83
+ "preload.js",
84
+ "bin/**/*",
85
+ "src/**/*",
86
+ "renderer/**/*",
87
+ "package.json"
88
+ ],
89
+ "asarUnpack": [
90
+ "bin/**/*",
91
+ "node_modules/node-pty/**/*"
92
+ ],
93
+ "win": {
94
+ "target": [
95
+ {
96
+ "target": "portable",
97
+ "arch": [
98
+ "x64"
99
+ ]
100
+ },
101
+ {
102
+ "target": "nsis",
103
+ "arch": [
104
+ "x64"
105
+ ]
106
+ }
107
+ ]
108
+ },
109
+ "mac": {
110
+ "category": "public.app-category.developer-tools",
111
+ "target": [
112
+ {
113
+ "target": "dmg",
114
+ "arch": ["arm64", "x64"]
115
+ },
116
+ {
117
+ "target": "zip",
118
+ "arch": ["arm64", "x64"]
119
+ }
120
+ ],
121
+ "artifactName": "LoadToAgent-${version}-${arch}.${ext}"
122
+ },
123
+ "portable": {
124
+ "artifactName": "LoadToAgent-${version}-portable.exe"
125
+ },
126
+ "nsis": {
127
+ "artifactName": "LoadToAgent-Setup-${version}.exe",
128
+ "oneClick": false,
129
+ "perMachine": false,
130
+ "allowToChangeInstallationDirectory": true
131
+ }
132
+ }
133
+ }