loadtoagent 1.0.0 → 1.3.1

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 (86) hide show
  1. package/README.ko.md +7 -0
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +7 -0
  4. package/docs/PROVIDER-CONTRACTS.md +23 -1
  5. package/docs/UI-AUDIT-100.md +118 -0
  6. package/docs/UI-AUDIT-101-200.md +120 -0
  7. package/docs/UI-AUDIT-201-300.md +120 -0
  8. package/main.js +183 -37
  9. package/package.json +15 -4
  10. package/preload.js +11 -0
  11. package/renderer/app-agent-actions.js +125 -53
  12. package/renderer/app-bootstrap.js +54 -9
  13. package/renderer/app-dashboard.js +197 -58
  14. package/renderer/app-drawer-content.js +103 -86
  15. package/renderer/app-drawer-data.js +26 -13
  16. package/renderer/app-drawer.js +79 -55
  17. package/renderer/app-events-dialogs.js +146 -37
  18. package/renderer/app-events-filters.js +172 -9
  19. package/renderer/app-events-navigation.js +78 -31
  20. package/renderer/app-events-sessions.js +171 -4
  21. package/renderer/app-events.js +2 -1
  22. package/renderer/app-graph-model.js +32 -4
  23. package/renderer/app-graph-orchestration.js +16 -13
  24. package/renderer/app-graph-view.js +228 -112
  25. package/renderer/app-management.js +211 -0
  26. package/renderer/app-provider-visibility.js +124 -0
  27. package/renderer/app-quality.js +568 -0
  28. package/renderer/app-run-modal.js +103 -33
  29. package/renderer/app-runtime-overview.js +312 -0
  30. package/renderer/app-session-render.js +94 -37
  31. package/renderer/app-tmux-render.js +119 -38
  32. package/renderer/app.js +155 -76
  33. package/renderer/i18n-messages.js +833 -15
  34. package/renderer/i18n.js +104 -0
  35. package/renderer/index.html +162 -75
  36. package/renderer/shared.js +17 -0
  37. package/renderer/styles-agent-map.css +50 -1
  38. package/renderer/styles-cards.css +26 -0
  39. package/renderer/styles-collaboration.css +18 -88
  40. package/renderer/styles-components.css +126 -14
  41. package/renderer/styles-management.css +355 -0
  42. package/renderer/styles-overlays.css +13 -2
  43. package/renderer/styles-product.css +24 -16
  44. package/renderer/styles-quality.css +312 -0
  45. package/renderer/styles-readability.css +862 -0
  46. package/renderer/styles-responsive-product.css +84 -12
  47. package/renderer/styles-responsive-runtime.css +31 -14
  48. package/renderer/styles-responsive-shell.css +44 -48
  49. package/renderer/styles-responsive-workflows.css +37 -17
  50. package/renderer/styles-run-composer.css +5 -0
  51. package/renderer/styles-runtime-overview.css +285 -0
  52. package/renderer/styles-settings.css +160 -0
  53. package/renderer/styles-terminal.css +339 -2
  54. package/renderer/styles-tmux.css +154 -0
  55. package/renderer/styles-workflow-map.css +362 -15
  56. package/renderer/styles-workflows.css +69 -2
  57. package/renderer/styles.css +27 -12
  58. package/renderer/terminal-agent.js +17 -13
  59. package/renderer/terminal-events.js +233 -40
  60. package/renderer/terminal-workbench.js +176 -78
  61. package/renderer/terminal.js +350 -37
  62. package/src/agentMonitor/claudeParser.js +96 -7
  63. package/src/agentMonitor/codexParser.js +59 -4
  64. package/src/agentMonitor/executionActivity.js +282 -0
  65. package/src/agentMonitor/genericParser.js +56 -13
  66. package/src/agentMonitor/hierarchy.js +17 -0
  67. package/src/agentMonitor/responseIntent.js +51 -0
  68. package/src/agentMonitor.js +21 -3
  69. package/src/agentRunner.js +66 -1
  70. package/src/attentionNotifier.js +71 -0
  71. package/src/automationMonitor.js +258 -0
  72. package/src/contracts.js +10 -1
  73. package/src/ipc/registerAgentIpc.js +9 -3
  74. package/src/ipc/registerAppIpc.js +4 -1
  75. package/src/ipc/registerTerminalIpc.js +12 -6
  76. package/src/macUpdateHelper.js +210 -0
  77. package/src/monitorWorker.js +65 -6
  78. package/src/platformPath.js +80 -0
  79. package/src/processMonitor.js +81 -23
  80. package/src/providerVisibilityStore.js +45 -0
  81. package/src/sessionIntelligence.js +247 -0
  82. package/src/terminalHost.js +381 -0
  83. package/src/terminalHostDaemon.js +60 -0
  84. package/src/terminalManager.js +158 -3
  85. package/src/tmuxMonitor.js +2 -2
  86. package/src/updateInstaller.js +175 -0
package/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { app, BrowserWindow, ipcMain, dialog, shell, clipboard, Tray, Menu, net } = require('electron');
3
+ const { app, BrowserWindow, ipcMain, dialog, shell, clipboard, Tray, Menu, net, Notification } = require('electron');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
@@ -10,10 +10,11 @@ const { execFileSync } = require('child_process');
10
10
  const { AgentRunner, probeProviders } = require('./src/agentRunner');
11
11
  const { providerList, blankUsage } = require('./src/providerRegistry');
12
12
  const { TerminalManager } = require('./src/terminalManager');
13
+ const { TerminalHostClient, launchTerminalHost } = require('./src/terminalHost');
13
14
  const { TmuxController } = require('./src/tmuxController');
14
15
  const { normalizeWslList } = require('./src/tmuxMonitor');
15
- const { BridgeServer } = require('./src/bridgeServer');
16
16
  const { UpdateManager } = require('./src/updateManager');
17
+ const { launchDownloadedUpdate } = require('./src/updateInstaller');
17
18
  const { readWorkspaces, removeWorkspace, writeWorkspaces } = require('./src/workspaceStore');
18
19
  const { registerAppIpc } = require('./src/ipc/registerAppIpc');
19
20
  const { registerAgentIpc } = require('./src/ipc/registerAgentIpc');
@@ -21,51 +22,69 @@ const { registerTerminalIpc } = require('./src/ipc/registerTerminalIpc');
21
22
  const { registerTmuxIpc } = require('./src/ipc/registerTmuxIpc');
22
23
  const { registerWorkspaceIpc } = require('./src/ipc/registerWorkspaceIpc');
23
24
  const { reportRecoverableError } = require('./src/diagnostics');
25
+ const { AttentionNotifier } = require('./src/attentionNotifier');
26
+ const { ProviderVisibilityStore } = require('./src/providerVisibilityStore');
27
+ const { macPathEntries } = require('./src/platformPath');
28
+
29
+ const PRODUCT_NAME = 'LoadToAgent';
30
+ app.setName(PRODUCT_NAME);
31
+ process.title = PRODUCT_NAME;
32
+ if (process.platform === 'win32') app.setAppUserModelId('com.wincube.loadtoagent');
24
33
 
25
34
  const demoCapture = process.env.LOADTOAGENT_DEMO_CAPTURE === '1';
26
35
  let mainWindow = null;
27
36
  let monitorWorker = null;
28
37
  let runner = null;
29
38
  let terminalManager = null;
30
- let bridgeServer = null;
31
39
  let bridgeLauncher = null;
32
40
  let backgroundTray = null;
33
41
  let updateManager = null;
42
+ let attentionNotifier = null;
34
43
  let isQuitting = false;
35
44
  let appLocale = 'ko';
45
+ let providerVisibilityStore = null;
46
+ let pendingAttentionSessionId = '';
47
+ let rendererBootstrapped = false;
36
48
  const tmuxController = new TmuxController({ platform: process.platform });
37
49
  let availability = {};
38
50
  let detailRequestId = 0;
39
51
  const pendingDetails = new Map();
40
52
  const MAIN_COPY = {
41
53
  ko: {
42
- trayTooltip: 'LoadToAgent · 백그라운드 AI 세션 {count}개',
54
+ trayTooltip: 'LoadToAgent · 백그라운드 터미널 {count}개',
43
55
  trayOpen: 'LoadToAgent 열기',
44
- traySessions: '백그라운드 AI 세션 {count}개 유지 중',
45
- trayQuit: '프로그램 끝내기 · AI 세션도 종료',
56
+ traySessions: '백그라운드 터미널 {count}개 유지 중',
57
+ trayQuit: '프로그램 끝내기 · 터미널 세션 유지',
46
58
  addWorkspaces: 'AI 작업 폴더 선택',
47
59
  pickWorkspace: '작업 폴더 선택',
60
+ attentionTitle: '내 확인이 필요합니다',
61
+ attentionBody: '{provider} · {title}',
48
62
  },
49
63
  en: {
50
- trayTooltip: 'LoadToAgent · {count} background AI sessions',
64
+ trayTooltip: 'LoadToAgent · {count} background terminals',
51
65
  trayOpen: 'Open LoadToAgent',
52
- traySessions: '{count} background AI sessions active',
53
- trayQuit: 'Quit app · End AI sessions too',
66
+ traySessions: '{count} background terminals active',
67
+ trayQuit: 'Quit app · Keep terminal sessions',
54
68
  addWorkspaces: 'Choose AI workspaces',
55
69
  pickWorkspace: 'Choose workspace',
70
+ attentionTitle: 'Your review is needed',
71
+ attentionBody: '{provider} · {title}',
56
72
  },
57
73
  'zh-CN': {
58
- trayTooltip: 'LoadToAgent · {count} 个后台 AI 会话',
74
+ trayTooltip: 'LoadToAgent · {count} 个后台终端',
59
75
  trayOpen: '打开 LoadToAgent',
60
- traySessions: '正在保持 {count} 个后台 AI 会话',
61
- trayQuit: '退出应用 · 同时结束 AI 会话',
76
+ traySessions: '正在保持 {count} 个后台终端',
77
+ trayQuit: '退出应用 · 保留终端会话',
62
78
  addWorkspaces: '选择 AI 工作文件夹',
63
79
  pickWorkspace: '选择工作文件夹',
80
+ attentionTitle: '需要你的确认',
81
+ attentionBody: '{provider} · {title}',
64
82
  },
65
83
  };
66
84
  let lastSnapshot = {
67
85
  generatedAt: new Date().toISOString(),
68
86
  sessions: [],
87
+ automations: [],
69
88
  tmux: { generatedAt: new Date().toISOString(), available: false, status: '확인 중', distros: [], summary: { distros: 0, sessions: 0, windows: 0, panes: 0, aiPanes: 0, linked: 0 } },
70
89
  summary: {
71
90
  providers: providerList().map(provider => ({ ...provider, installed: false, sessions: 0, active: 0, waiting: 0, subagents: 0, usage: blankUsage() })),
@@ -120,6 +139,30 @@ function listWorkspaces() {
120
139
  return readWorkspaces(userFile('workspaces.json'));
121
140
  }
122
141
 
142
+ function isProviderVisible(providerId) {
143
+ return providerVisibilityStore ? providerVisibilityStore.isVisible(providerId) : true;
144
+ }
145
+
146
+ function loadProviderVisibility() {
147
+ providerVisibilityStore = new ProviderVisibilityStore(
148
+ userFile('provider-visibility.json'),
149
+ providerList().map(provider => provider.id),
150
+ error => reportRecoverableError('provider-visibility-load', error),
151
+ );
152
+ return providerVisibilityStore.load();
153
+ }
154
+
155
+ function saveProviderVisibility(value = {}) {
156
+ if (!providerVisibilityStore) loadProviderVisibility();
157
+ const saved = providerVisibilityStore.save(value);
158
+ updateBackgroundTrayMenu();
159
+ return saved;
160
+ }
161
+
162
+ function visibleSnapshotSessions(snapshot = lastSnapshot) {
163
+ return { ...snapshot, sessions: (snapshot.sessions || []).filter(session => isProviderVisible(session.provider)) };
164
+ }
165
+
123
166
  function saveWorkspaces(items) {
124
167
  return writeWorkspaces(userFile('workspaces.json'), items);
125
168
  }
@@ -141,18 +184,11 @@ function listWslDistros() {
141
184
 
142
185
  function hydratePlatformPath() {
143
186
  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);
187
+ process.env.PATH = macPathEntries(os.homedir(), process.env.PATH).join(path.delimiter);
153
188
  }
154
189
 
155
190
  function createWindow() {
191
+ rendererBootstrapped = false;
156
192
  mainWindow = new BrowserWindow({
157
193
  width: 1600,
158
194
  height: 980,
@@ -169,13 +205,14 @@ function createWindow() {
169
205
  },
170
206
  });
171
207
  mainWindow.setMenuBarVisibility(false);
208
+ mainWindow.webContents.on('did-start-loading', () => { rendererBootstrapped = false; });
172
209
  mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
173
210
  mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
174
211
  const allowedUrl = pathToFileURL(path.join(__dirname, 'renderer', 'index.html')).href;
175
212
  mainWindow.webContents.on('will-navigate', (event, url) => { if (url !== allowedUrl) event.preventDefault(); });
176
213
  mainWindow.once('ready-to-show', () => mainWindow && mainWindow.show());
177
214
  mainWindow.on('close', event => {
178
- if (isQuitting || !backgroundAgentSessions().length) return;
215
+ if (isQuitting || !backgroundTerminalSessions().length) return;
179
216
  event.preventDefault();
180
217
  mainWindow.hide();
181
218
  ensureBackgroundTray();
@@ -183,9 +220,13 @@ function createWindow() {
183
220
  mainWindow.on('closed', () => { mainWindow = null; });
184
221
  }
185
222
 
186
- function backgroundAgentSessions() {
223
+ function backgroundTerminalSessions() {
187
224
  if (!terminalManager) return [];
188
- return terminalManager.list().filter(session => session.type === 'agent' && (session.status === 'running' || session.status === 'starting'));
225
+ return terminalManager.list().filter(session => session.status === 'running' || session.status === 'starting');
226
+ }
227
+
228
+ function visibleTerminalSessions(sessions) {
229
+ return (sessions || []).filter(session => session.type !== 'agent' || isProviderVisible(session.provider));
189
230
  }
190
231
 
191
232
  function showMainWindow() {
@@ -197,7 +238,7 @@ function showMainWindow() {
197
238
 
198
239
  function updateBackgroundTrayMenu() {
199
240
  if (!backgroundTray) return;
200
- const count = backgroundAgentSessions().length;
241
+ const count = backgroundTerminalSessions().length;
201
242
  backgroundTray.setToolTip(mainText('trayTooltip', { count }));
202
243
  backgroundTray.setContextMenu(Menu.buildFromTemplate([
203
244
  { label: mainText('trayOpen'), click: showMainWindow },
@@ -254,6 +295,56 @@ function sendSnapshot(snapshot) {
254
295
  try { mainWindow.webContents.send('agents:snapshot', snapshot); } catch (error) { reportRecoverableError('ipc-send:agents:snapshot', error); }
255
296
  }
256
297
 
298
+ function openAttentionSession(session) {
299
+ if (!isProviderVisible(session && session.provider)) return;
300
+ pendingAttentionSessionId = String(session && session.id || '');
301
+ showMainWindow();
302
+ if (!mainWindow || mainWindow.isDestroyed()) return;
303
+ mainWindow.flashFrame(false);
304
+ if (!rendererBootstrapped || mainWindow.webContents.isLoadingMainFrame()) return;
305
+ try {
306
+ mainWindow.webContents.send('agents:attention-requested', { sessionId: pendingAttentionSessionId });
307
+ pendingAttentionSessionId = '';
308
+ } catch (error) {
309
+ reportRecoverableError('ipc-send:agents:attention-requested', error);
310
+ }
311
+ }
312
+
313
+ function markRendererReady() {
314
+ rendererBootstrapped = true;
315
+ if (!pendingAttentionSessionId || !mainWindow || mainWindow.isDestroyed()) return { ok: true };
316
+ const sessionId = pendingAttentionSessionId;
317
+ try {
318
+ mainWindow.webContents.send('agents:attention-requested', { sessionId });
319
+ pendingAttentionSessionId = '';
320
+ } catch (error) {
321
+ reportRecoverableError('ipc-send:agents:attention-requested', error);
322
+ }
323
+ return { ok: true };
324
+ }
325
+
326
+ function createAttentionNotifier() {
327
+ return new AttentionNotifier({
328
+ Notification,
329
+ isSupported: () => Notification.isSupported(),
330
+ copy: session => {
331
+ const provider = providerList().find(item => item.id === session.provider);
332
+ return {
333
+ title: mainText('attentionTitle'),
334
+ body: mainText('attentionBody', {
335
+ provider: provider && provider.label || session.provider || 'AI',
336
+ title: session.title || '이름 없는 세션',
337
+ }),
338
+ };
339
+ },
340
+ onOpen: openAttentionSession,
341
+ onFallback: session => {
342
+ if (mainWindow && !mainWindow.isDestroyed()) mainWindow.flashFrame(true);
343
+ openAttentionSession(session);
344
+ },
345
+ });
346
+ }
347
+
257
348
  function sendUpdateState(update) {
258
349
  if (!mainWindow || mainWindow.isDestroyed()) return;
259
350
  try { mainWindow.webContents.send('app:update-state', update); } catch (error) { reportRecoverableError('ipc-send:app:update-state', error); }
@@ -264,10 +355,46 @@ function installationType() {
264
355
  return fs.existsSync(path.join(__dirname, '.git')) ? 'source' : 'npm';
265
356
  }
266
357
 
267
- function setupRuntime() {
358
+ async function installDownloadedUpdate() {
359
+ if (!updateManager) throw new Error('업데이트 관리자가 준비되지 않았습니다.');
360
+ const downloaded = await updateManager.download();
361
+ const outcome = await launchDownloadedUpdate({
362
+ platform: process.platform,
363
+ installType: process.env.PORTABLE_EXECUTABLE_FILE ? 'portable' : installationType(),
364
+ installerPath: downloaded.downloadedPath,
365
+ downloadsDir: path.join(app.getPath('userData'), 'updates'),
366
+ appPath: process.execPath,
367
+ parentPid: process.pid,
368
+ shell,
369
+ });
370
+ if (outcome.mode === 'automatic') {
371
+ isQuitting = true;
372
+ setImmediate(() => app.quit());
373
+ }
374
+ return { ...updateManager.getState(), installMode: outcome.mode };
375
+ }
376
+
377
+ async function setupRuntime() {
378
+ loadProviderVisibility();
268
379
  const runsDir = userFile('agent-runs');
269
380
  runner = new AgentRunner({ runsDir });
270
- terminalManager = new TerminalManager();
381
+ const terminalStoreFile = userFile('terminal-sessions.json');
382
+ const terminalHostFile = userFile('terminal-host.json');
383
+ terminalManager = demoCapture
384
+ ? new TerminalManager({
385
+ storeFile: terminalStoreFile,
386
+ onPersistenceError: (operation, error) => reportRecoverableError(`terminal-sessions:${operation}`, error),
387
+ })
388
+ : new TerminalHostClient({
389
+ discoveryFile: terminalHostFile,
390
+ spawnHost: () => launchTerminalHost({
391
+ executable: process.execPath,
392
+ script: path.join(__dirname, 'src', 'terminalHostDaemon.js'),
393
+ storeFile: terminalStoreFile,
394
+ discoveryFile: terminalHostFile,
395
+ bridgeHome,
396
+ }),
397
+ });
271
398
  updateManager = new UpdateManager({
272
399
  currentVersion: app.getVersion(),
273
400
  platform: process.platform,
@@ -278,6 +405,7 @@ function setupRuntime() {
278
405
  downloadsDir: path.join(app.getPath('userData'), 'updates'),
279
406
  });
280
407
  updateManager.on('state', sendUpdateState);
408
+ attentionNotifier = createAttentionNotifier();
281
409
  updateManager.check().catch(error => reportRecoverableError('startup-update-check', error));
282
410
  if (demoCapture) {
283
411
  availability = Object.fromEntries(providerList().map(provider => [provider.id, true]));
@@ -291,12 +419,16 @@ function setupRuntime() {
291
419
  }
292
420
  terminalManager.on('data', payload => sendTerminal('terminals:data', payload));
293
421
  terminalManager.on('state', payload => {
294
- sendTerminal('terminals:state', payload);
422
+ if (!payload.session || payload.session.type !== 'agent' || isProviderVisible(payload.session.provider)) {
423
+ sendTerminal('terminals:state', { ...payload, sessions: visibleTerminalSessions(payload.sessions) });
424
+ }
295
425
  updateBackgroundTrayMenu();
296
426
  if (monitorWorker) monitorWorker.postMessage({ type: 'bridge-presence', bridges: bridgePresence() });
297
427
  });
298
- bridgeServer = new BridgeServer({ terminalManager, home: bridgeHome, platform: process.platform });
299
- bridgeServer.start().catch(error => sendTerminal('terminals:error', { id: '', message: `외부 터미널 브리지를 열지 못했습니다: ${error.message}` }));
428
+ terminalManager.on('disconnect', () => {
429
+ sendTerminal('terminals:error', { id: '', message: '터미널 호스트 연결이 끊어졌습니다. 프로그램을 다시 시작하면 실행 중인 세션에 다시 연결합니다.' });
430
+ });
431
+ await terminalManager.connect();
300
432
  availability = probeProviders();
301
433
  monitorWorker = new Worker(path.join(__dirname, 'src', 'monitorWorker.js'), {
302
434
  workerData: { runsDir, home: os.homedir(), intervalMs: 1200, availability },
@@ -305,6 +437,7 @@ function setupRuntime() {
305
437
  monitorWorker.on('message', message => {
306
438
  if (message && message.type === 'snapshot') {
307
439
  lastSnapshot = message.snapshot;
440
+ attentionNotifier.sync(visibleSnapshotSessions(lastSnapshot));
308
441
  sendSnapshot(lastSnapshot);
309
442
  }
310
443
  if (message && message.type === 'detail-result') {
@@ -325,10 +458,11 @@ function bridgePresence() {
325
458
  if (!terminalManager) return [];
326
459
  const environment = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'macos' : 'linux');
327
460
  return terminalManager.list()
328
- .filter(session => session.type === 'agent' && session.status === 'running')
461
+ .filter(session => session.type === 'agent' && (session.status === 'running' || session.status === 'starting'))
329
462
  .map(session => ({
330
463
  id: session.bridgeId || session.id,
331
464
  bridgeId: session.bridgeId || '',
465
+ linkedSessionId: session.bridgeId || '',
332
466
  terminalId: session.id,
333
467
  provider: session.provider,
334
468
  pid: session.pid,
@@ -353,17 +487,20 @@ function bootstrapState() {
353
487
  id: process.platform,
354
488
  label: process.platform === 'darwin' ? 'macOS' : (process.platform === 'win32' ? 'Windows' : 'Linux'),
355
489
  localShell: process.platform === 'win32' ? 'powershell' : 'shell',
356
- localShellLabel: process.platform === 'darwin' ? 'Mac 명령창' : (process.platform === 'win32' ? 'Windows 명령창' : 'Linux 명령창'),
490
+ localShellLabel: process.platform === 'darwin' ? 'macOS shell' : (process.platform === 'win32' ? 'Windows PowerShell' : 'Linux shell'),
357
491
  nativeTmux: process.platform !== 'win32',
358
492
  },
359
493
  bridgeCli: bridgeLauncher,
360
494
  update: updateManager ? updateManager.getState() : null,
495
+ providerVisibility: providerVisibilityStore ? providerVisibilityStore.snapshot() : { hidden: [] },
361
496
  };
362
497
  }
363
498
 
364
499
  function requestAgentDetail(sessionId) {
365
500
  return new Promise(resolve => {
366
501
  if (!monitorWorker || String(sessionId || '').length > 500) return resolve(null);
502
+ const card = (lastSnapshot.sessions || []).find(session => session.id === String(sessionId || ''));
503
+ if (card && !isProviderVisible(card.provider)) return resolve(null);
367
504
  const requestId = ++detailRequestId;
368
505
  const timer = setTimeout(() => {
369
506
  if (!pendingDetails.has(requestId)) return;
@@ -384,9 +521,10 @@ function registerIpcHandlers() {
384
521
  registerAppIpc({
385
522
  handleTrusted,
386
523
  bootstrap: bootstrapState,
524
+ rendererReady: markRendererReady,
387
525
  backgroundState: () => ({
388
526
  visible: Boolean(mainWindow && !mainWindow.isDestroyed() && mainWindow.isVisible()),
389
- backgroundSessions: backgroundAgentSessions().length,
527
+ backgroundSessions: backgroundTerminalSessions().length,
390
528
  trayReady: Boolean(backgroundTray),
391
529
  }),
392
530
  show: () => { showMainWindow(); return { ok: true }; },
@@ -395,13 +533,16 @@ function registerIpcHandlers() {
395
533
  updateBackgroundTrayMenu();
396
534
  return { locale: appLocale };
397
535
  },
536
+ setProviderVisibility: saveProviderVisibility,
398
537
  updateManager: () => updateManager,
538
+ installUpdate: installDownloadedUpdate,
399
539
  });
400
540
  registerAgentIpc({
401
541
  handleTrusted,
402
542
  snapshot: () => { refreshMonitor(); return lastSnapshot; },
403
543
  requestDetail: requestAgentDetail,
404
544
  runner: () => runner,
545
+ isProviderVisible,
405
546
  probeProviders: () => {
406
547
  availability = probeProviders();
407
548
  if (monitorWorker) monitorWorker.postMessage({ type: 'availability', availability });
@@ -414,6 +555,7 @@ function registerIpcHandlers() {
414
555
  requireTrustedSender,
415
556
  trustedSender,
416
557
  manager: () => terminalManager,
558
+ isProviderVisible,
417
559
  listWslDistros,
418
560
  sendError: payload => sendTerminal('terminals:error', payload),
419
561
  });
@@ -466,11 +608,14 @@ function registerIpcHandlers() {
466
608
 
467
609
  registerIpcHandlers();
468
610
 
469
- app.whenReady().then(() => {
611
+ app.whenReady().then(async () => {
470
612
  hydratePlatformPath();
471
- setupRuntime();
613
+ await setupRuntime();
472
614
  createWindow();
473
615
  app.on('activate', showMainWindow);
616
+ }).catch(error => {
617
+ dialog.showErrorBox('LoadToAgent 시작 실패', error.stack || error.message || String(error));
618
+ app.quit();
474
619
  });
475
620
 
476
621
  app.on('window-all-closed', () => {
@@ -479,8 +624,9 @@ app.on('window-all-closed', () => {
479
624
 
480
625
  app.on('before-quit', () => {
481
626
  isQuitting = true;
482
- if (bridgeServer) bridgeServer.dispose();
483
- if (terminalManager) terminalManager.dispose();
627
+ if (attentionNotifier) attentionNotifier.dispose();
628
+ if (terminalManager instanceof TerminalHostClient) terminalManager.dispose({ shutdownIfIdle: true });
629
+ else if (terminalManager) terminalManager.dispose({ preserveSessions: true });
484
630
  if (monitorWorker) {
485
631
  monitorWorker.postMessage({ type: 'stop' });
486
632
  monitorWorker.terminate();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadtoagent",
3
- "version": "1.0.0",
3
+ "version": "1.3.1",
4
4
  "description": "A local desktop dashboard for Claude, Codex, Gemini, and Grok sessions.",
5
5
  "main": "main.js",
6
6
  "author": "wincube AX",
@@ -44,14 +44,20 @@
44
44
  "start": "electron .",
45
45
  "test": "node scripts/regression-test.js",
46
46
  "test:terminal": "electron scripts/terminal-integration-test.js",
47
+ "test:terminal-restart": "node scripts/terminal-restart-persistence-check.js",
47
48
  "test:bridge": "electron scripts/bridge-integration-test.js",
48
49
  "test:tmux": "node scripts/tmux-integration-test.js",
49
50
  "test:visual": "electron scripts/visual-check.js",
50
51
  "test:responsive": "electron scripts/responsive-check.js",
51
52
  "test:interaction": "electron scripts/interaction-check.js",
53
+ "test:scroll": "electron scripts/scroll-retention-check.js",
54
+ "audit:ui": "electron scripts/browser-audit-host.js",
55
+ "test:runtime-overview": "electron scripts/runtime-overview-visual.js",
56
+ "test:readability": "electron scripts/readability-visual-check.js",
57
+ "test:update:mac": "node scripts/mac-update-integration-test.js",
52
58
  "demo:readme": "electron scripts/readme-demo.js",
53
59
  "dist": "electron-builder",
54
- "dist:win": "electron-builder --win portable",
60
+ "dist:win": "electron-builder --win --publish never",
55
61
  "dist:mac": "electron-builder --mac --publish never",
56
62
  "pack": "electron-builder --dir",
57
63
  "prepublishOnly": "npm test"
@@ -90,7 +96,9 @@
90
96
  "bin/**/*",
91
97
  "node_modules/node-pty/**/*"
92
98
  ],
99
+ "afterPack": "scripts/after-pack.js",
93
100
  "win": {
101
+ "icon": "build/icon.ico",
94
102
  "target": [
95
103
  {
96
104
  "target": "portable",
@@ -107,6 +115,7 @@
107
115
  ]
108
116
  },
109
117
  "mac": {
118
+ "icon": "build/icon.png",
110
119
  "category": "public.app-category.developer-tools",
111
120
  "target": [
112
121
  {
@@ -121,13 +130,15 @@
121
130
  "artifactName": "LoadToAgent-${version}-${arch}.${ext}"
122
131
  },
123
132
  "portable": {
124
- "artifactName": "LoadToAgent-${version}-portable.exe"
133
+ "artifactName": "LoadToAgent-${version}-portable.exe",
134
+ "unpackDirName": false
125
135
  },
126
136
  "nsis": {
127
137
  "artifactName": "LoadToAgent-Setup-${version}.exe",
128
138
  "oneClick": false,
129
139
  "perMachine": false,
130
- "allowToChangeInstallationDirectory": true
140
+ "allowToChangeInstallationDirectory": true,
141
+ "runAfterFinish": false
131
142
  }
132
143
  }
133
144
  }
package/preload.js CHANGED
@@ -4,17 +4,23 @@ const { contextBridge, ipcRenderer } = require('electron');
4
4
 
5
5
  contextBridge.exposeInMainWorld('loadtoagent', {
6
6
  bootstrap: () => ipcRenderer.invoke('app:bootstrap'),
7
+ rendererReady: () => ipcRenderer.invoke('app:renderer-ready'),
7
8
  backgroundState: () => ipcRenderer.invoke('app:background-state'),
8
9
  showApp: () => ipcRenderer.invoke('app:show'),
9
10
  setLocale: locale => ipcRenderer.invoke('app:set-locale', locale),
11
+ setProviderVisibility: preference => ipcRenderer.invoke('app:set-provider-visibility', preference),
10
12
  checkForUpdate: () => ipcRenderer.invoke('app:update-check'),
11
13
  downloadUpdate: () => ipcRenderer.invoke('app:update-download'),
12
14
  openDownloadedUpdate: () => ipcRenderer.invoke('app:update-open'),
15
+ installDownloadedUpdate: () => ipcRenderer.invoke('app:update-install'),
13
16
  openUpdateRelease: () => ipcRenderer.invoke('app:update-open-release'),
14
17
  snapshot: () => ipcRenderer.invoke('agents:snapshot'),
15
18
  sessionDetail: sessionId => ipcRenderer.invoke('agents:detail', sessionId),
16
19
  runAgent: options => ipcRenderer.invoke('agents:run', options),
17
20
  stopAgent: runId => ipcRenderer.invoke('agents:stop', runId),
21
+ pauseAgent: runId => ipcRenderer.invoke('agents:pause', runId),
22
+ resumeAgentRun: runId => ipcRenderer.invoke('agents:resume-run', runId),
23
+ retryAgent: runId => ipcRenderer.invoke('agents:retry', runId),
18
24
  activeRuns: () => ipcRenderer.invoke('agents:active-runs'),
19
25
  probeProviders: () => ipcRenderer.invoke('providers:probe'),
20
26
  listWorkspaces: () => ipcRenderer.invoke('workspaces:list'),
@@ -67,6 +73,11 @@ contextBridge.exposeInMainWorld('loadtoagent', {
67
73
  ipcRenderer.on('agents:snapshot', handler);
68
74
  return () => ipcRenderer.removeListener('agents:snapshot', handler);
69
75
  },
76
+ onAttentionRequested: callback => {
77
+ const handler = (_event, payload) => callback(payload);
78
+ ipcRenderer.on('agents:attention-requested', handler);
79
+ return () => ipcRenderer.removeListener('agents:attention-requested', handler);
80
+ },
70
81
  onUpdateState: callback => {
71
82
  const handler = (_event, update) => callback(update);
72
83
  ipcRenderer.on('app:update-state', handler);