loadtoagent 1.1.0 → 1.3.2

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 +14 -5
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +7 -0
  4. package/docs/PROVIDER-CONTRACTS.md +25 -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 +157 -38
  9. package/package.json +13 -4
  10. package/preload.js +13 -2
  11. package/renderer/app-agent-actions.js +125 -53
  12. package/renderer/app-bootstrap.js +54 -19
  13. package/renderer/app-dashboard.js +196 -55
  14. package/renderer/app-drawer-content.js +48 -44
  15. package/renderer/app-drawer-data.js +26 -13
  16. package/renderer/app-drawer.js +72 -54
  17. package/renderer/app-events-dialogs.js +146 -37
  18. package/renderer/app-events-filters.js +172 -13
  19. package/renderer/app-events-navigation.js +77 -30
  20. package/renderer/app-events-sessions.js +156 -9
  21. package/renderer/app-events.js +2 -1
  22. package/renderer/app-graph-model.js +20 -38
  23. package/renderer/app-graph-orchestration.js +15 -13
  24. package/renderer/app-graph-view.js +228 -113
  25. package/renderer/app-management.js +257 -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 +104 -34
  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 +52 -44
  32. package/renderer/app.js +153 -75
  33. package/renderer/i18n-messages.js +867 -37
  34. package/renderer/i18n.js +104 -0
  35. package/renderer/index.html +154 -67
  36. package/renderer/shared.js +17 -0
  37. package/renderer/styles-agent-map.css +16 -10
  38. package/renderer/styles-cards.css +44 -18
  39. package/renderer/styles-collaboration.css +23 -23
  40. package/renderer/styles-components.css +122 -45
  41. package/renderer/styles-management.css +364 -0
  42. package/renderer/styles-onboarding.css +8 -8
  43. package/renderer/styles-overlays.css +13 -7
  44. package/renderer/styles-product.css +48 -40
  45. package/renderer/styles-quality.css +312 -0
  46. package/renderer/styles-readability.css +1040 -0
  47. package/renderer/styles-responsive-product.css +84 -12
  48. package/renderer/styles-responsive-runtime.css +25 -17
  49. package/renderer/styles-responsive-shell.css +44 -48
  50. package/renderer/styles-responsive-workflows.css +39 -5
  51. package/renderer/styles-run-composer.css +28 -23
  52. package/renderer/styles-runtime-overview.css +285 -0
  53. package/renderer/styles-settings.css +176 -16
  54. package/renderer/styles-terminal.css +374 -37
  55. package/renderer/styles-tmux.css +49 -49
  56. package/renderer/styles-workflow-map.css +362 -15
  57. package/renderer/styles-workflows.css +116 -49
  58. package/renderer/styles.css +34 -19
  59. package/renderer/terminal-agent.js +17 -13
  60. package/renderer/terminal-events.js +253 -37
  61. package/renderer/terminal-workbench.js +208 -89
  62. package/renderer/terminal.js +350 -35
  63. package/src/agentMonitor/claudeParser.js +96 -7
  64. package/src/agentMonitor/codexParser.js +59 -4
  65. package/src/agentMonitor/executionActivity.js +282 -0
  66. package/src/agentMonitor/genericParser.js +56 -13
  67. package/src/agentMonitor/hierarchy.js +17 -0
  68. package/src/agentMonitor/responseIntent.js +51 -0
  69. package/src/agentMonitor.js +21 -3
  70. package/src/agentRunner.js +66 -1
  71. package/src/attentionNotifier.js +14 -10
  72. package/src/automationMonitor.js +258 -0
  73. package/src/contracts.js +10 -1
  74. package/src/ipc/registerAgentIpc.js +9 -3
  75. package/src/ipc/registerAppIpc.js +4 -1
  76. package/src/ipc/registerTerminalIpc.js +12 -14
  77. package/src/macUpdateHelper.js +210 -0
  78. package/src/monitorWorker.js +65 -6
  79. package/src/platformPath.js +80 -0
  80. package/src/processMonitor.js +80 -22
  81. package/src/providerVisibilityStore.js +45 -0
  82. package/src/sessionIntelligence.js +261 -0
  83. package/src/terminalHost.js +453 -0
  84. package/src/terminalHostDaemon.js +61 -0
  85. package/src/terminalManager.js +218 -6
  86. package/src/updateInstaller.js +175 -0
package/main.js CHANGED
@@ -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, resolveTerminalHostExecutable } = 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');
@@ -22,6 +23,8 @@ const { registerTmuxIpc } = require('./src/ipc/registerTmuxIpc');
22
23
  const { registerWorkspaceIpc } = require('./src/ipc/registerWorkspaceIpc');
23
24
  const { reportRecoverableError } = require('./src/diagnostics');
24
25
  const { AttentionNotifier } = require('./src/attentionNotifier');
26
+ const { ProviderVisibilityStore } = require('./src/providerVisibilityStore');
27
+ const { macPathEntries } = require('./src/platformPath');
25
28
 
26
29
  const PRODUCT_NAME = 'LoadToAgent';
27
30
  app.setName(PRODUCT_NAME);
@@ -33,52 +36,64 @@ let mainWindow = null;
33
36
  let monitorWorker = null;
34
37
  let runner = null;
35
38
  let terminalManager = null;
36
- let bridgeServer = null;
37
39
  let bridgeLauncher = null;
38
40
  let backgroundTray = null;
39
41
  let updateManager = null;
40
42
  let attentionNotifier = null;
41
43
  let isQuitting = false;
42
44
  let appLocale = 'ko';
45
+ let providerVisibilityStore = null;
46
+ let pendingAttentionSessionId = '';
47
+ let rendererBootstrapped = false;
43
48
  const tmuxController = new TmuxController({ platform: process.platform });
44
49
  let availability = {};
45
50
  let detailRequestId = 0;
46
51
  const pendingDetails = new Map();
47
52
  const MAIN_COPY = {
48
53
  ko: {
49
- trayTooltip: 'LoadToAgent · 백그라운드 AI 세션 {count}개',
54
+ trayTooltip: 'LoadToAgent · 백그라운드 터미널 {count}개',
50
55
  trayOpen: 'LoadToAgent 열기',
51
- traySessions: '백그라운드 AI 세션 {count}개 유지 중',
52
- trayQuit: '프로그램 끝내기 · AI 세션도 종료',
56
+ traySessions: '백그라운드 터미널 {count}개 유지 중',
57
+ trayQuit: '프로그램 끝내기 · 터미널 세션 유지',
53
58
  addWorkspaces: 'AI 작업 폴더 선택',
54
59
  pickWorkspace: '작업 폴더 선택',
55
60
  attentionTitle: '내 확인이 필요합니다',
56
61
  attentionBody: '{provider} · {title}',
62
+ terminalHostReconnecting: '터미널 연결을 자동으로 복구하는 중입니다.',
63
+ terminalHostReconnected: '터미널 연결을 복구했습니다.',
64
+ terminalHostReconnectFailed: '터미널 연결을 복구하지 못했습니다: {reason}',
57
65
  },
58
66
  en: {
59
- trayTooltip: 'LoadToAgent · {count} background AI sessions',
67
+ trayTooltip: 'LoadToAgent · {count} background terminals',
60
68
  trayOpen: 'Open LoadToAgent',
61
- traySessions: '{count} background AI sessions active',
62
- trayQuit: 'Quit app · End AI sessions too',
69
+ traySessions: '{count} background terminals active',
70
+ trayQuit: 'Quit app · Keep terminal sessions',
63
71
  addWorkspaces: 'Choose AI workspaces',
64
72
  pickWorkspace: 'Choose workspace',
65
73
  attentionTitle: 'Your review is needed',
66
74
  attentionBody: '{provider} · {title}',
75
+ terminalHostReconnecting: 'Restoring the terminal connection automatically.',
76
+ terminalHostReconnected: 'Terminal connection restored.',
77
+ terminalHostReconnectFailed: 'Could not restore the terminal connection: {reason}',
67
78
  },
68
79
  'zh-CN': {
69
- trayTooltip: 'LoadToAgent · {count} 个后台 AI 会话',
80
+ trayTooltip: 'LoadToAgent · {count} 个后台终端',
70
81
  trayOpen: '打开 LoadToAgent',
71
- traySessions: '正在保持 {count} 个后台 AI 会话',
72
- trayQuit: '退出应用 · 同时结束 AI 会话',
82
+ traySessions: '正在保持 {count} 个后台终端',
83
+ trayQuit: '退出应用 · 保留终端会话',
73
84
  addWorkspaces: '选择 AI 工作文件夹',
74
85
  pickWorkspace: '选择工作文件夹',
75
86
  attentionTitle: '需要你的确认',
76
87
  attentionBody: '{provider} · {title}',
88
+ terminalHostReconnecting: '正在自动恢复终端连接。',
89
+ terminalHostReconnected: '终端连接已恢复。',
90
+ terminalHostReconnectFailed: '无法恢复终端连接:{reason}',
77
91
  },
78
92
  };
79
93
  let lastSnapshot = {
80
94
  generatedAt: new Date().toISOString(),
81
95
  sessions: [],
96
+ automations: [],
82
97
  tmux: { generatedAt: new Date().toISOString(), available: false, status: '확인 중', distros: [], summary: { distros: 0, sessions: 0, windows: 0, panes: 0, aiPanes: 0, linked: 0 } },
83
98
  summary: {
84
99
  providers: providerList().map(provider => ({ ...provider, installed: false, sessions: 0, active: 0, waiting: 0, subagents: 0, usage: blankUsage() })),
@@ -133,6 +148,30 @@ function listWorkspaces() {
133
148
  return readWorkspaces(userFile('workspaces.json'));
134
149
  }
135
150
 
151
+ function isProviderVisible(providerId) {
152
+ return providerVisibilityStore ? providerVisibilityStore.isVisible(providerId) : true;
153
+ }
154
+
155
+ function loadProviderVisibility() {
156
+ providerVisibilityStore = new ProviderVisibilityStore(
157
+ userFile('provider-visibility.json'),
158
+ providerList().map(provider => provider.id),
159
+ error => reportRecoverableError('provider-visibility-load', error),
160
+ );
161
+ return providerVisibilityStore.load();
162
+ }
163
+
164
+ function saveProviderVisibility(value = {}) {
165
+ if (!providerVisibilityStore) loadProviderVisibility();
166
+ const saved = providerVisibilityStore.save(value);
167
+ updateBackgroundTrayMenu();
168
+ return saved;
169
+ }
170
+
171
+ function visibleSnapshotSessions(snapshot = lastSnapshot) {
172
+ return { ...snapshot, sessions: (snapshot.sessions || []).filter(session => isProviderVisible(session.provider)) };
173
+ }
174
+
136
175
  function saveWorkspaces(items) {
137
176
  return writeWorkspaces(userFile('workspaces.json'), items);
138
177
  }
@@ -154,18 +193,11 @@ function listWslDistros() {
154
193
 
155
194
  function hydratePlatformPath() {
156
195
  if (process.platform !== 'darwin') return;
157
- const additions = ['/opt/homebrew/bin', '/usr/local/bin', path.join(os.homedir(), '.local', 'bin')];
158
- try {
159
- const shellPath = process.env.SHELL || '/bin/zsh';
160
- const loginPath = execFileSync(shellPath, ['-lic', 'printf %s "$PATH"'], { encoding: 'utf8', timeout: 5_000 }).trim();
161
- additions.unshift(...loginPath.split(path.delimiter));
162
- } catch (error) {
163
- reportRecoverableError('platform-path', error);
164
- }
165
- process.env.PATH = [...new Set([...additions, ...String(process.env.PATH || '').split(path.delimiter)].filter(Boolean))].join(path.delimiter);
196
+ process.env.PATH = macPathEntries(os.homedir(), process.env.PATH).join(path.delimiter);
166
197
  }
167
198
 
168
199
  function createWindow() {
200
+ rendererBootstrapped = false;
169
201
  mainWindow = new BrowserWindow({
170
202
  width: 1600,
171
203
  height: 980,
@@ -182,13 +214,14 @@ function createWindow() {
182
214
  },
183
215
  });
184
216
  mainWindow.setMenuBarVisibility(false);
217
+ mainWindow.webContents.on('did-start-loading', () => { rendererBootstrapped = false; });
185
218
  mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
186
219
  mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
187
220
  const allowedUrl = pathToFileURL(path.join(__dirname, 'renderer', 'index.html')).href;
188
221
  mainWindow.webContents.on('will-navigate', (event, url) => { if (url !== allowedUrl) event.preventDefault(); });
189
222
  mainWindow.once('ready-to-show', () => mainWindow && mainWindow.show());
190
223
  mainWindow.on('close', event => {
191
- if (isQuitting || !backgroundAgentSessions().length) return;
224
+ if (isQuitting || !backgroundTerminalSessions().length) return;
192
225
  event.preventDefault();
193
226
  mainWindow.hide();
194
227
  ensureBackgroundTray();
@@ -196,9 +229,13 @@ function createWindow() {
196
229
  mainWindow.on('closed', () => { mainWindow = null; });
197
230
  }
198
231
 
199
- function backgroundAgentSessions() {
232
+ function backgroundTerminalSessions() {
200
233
  if (!terminalManager) return [];
201
- return terminalManager.list().filter(session => session.type === 'agent' && (session.status === 'running' || session.status === 'starting'));
234
+ return terminalManager.list().filter(session => session.status === 'running' || session.status === 'starting');
235
+ }
236
+
237
+ function visibleTerminalSessions(sessions) {
238
+ return (sessions || []).filter(session => session.type !== 'agent' || isProviderVisible(session.provider));
202
239
  }
203
240
 
204
241
  function showMainWindow() {
@@ -210,7 +247,7 @@ function showMainWindow() {
210
247
 
211
248
  function updateBackgroundTrayMenu() {
212
249
  if (!backgroundTray) return;
213
- const count = backgroundAgentSessions().length;
250
+ const count = backgroundTerminalSessions().length;
214
251
  backgroundTray.setToolTip(mainText('trayTooltip', { count }));
215
252
  backgroundTray.setContextMenu(Menu.buildFromTemplate([
216
253
  { label: mainText('trayOpen'), click: showMainWindow },
@@ -268,14 +305,31 @@ function sendSnapshot(snapshot) {
268
305
  }
269
306
 
270
307
  function openAttentionSession(session) {
308
+ if (!isProviderVisible(session && session.provider)) return;
309
+ pendingAttentionSessionId = String(session && session.id || '');
271
310
  showMainWindow();
272
311
  if (!mainWindow || mainWindow.isDestroyed()) return;
273
312
  mainWindow.flashFrame(false);
313
+ if (!rendererBootstrapped || mainWindow.webContents.isLoadingMainFrame()) return;
314
+ try {
315
+ mainWindow.webContents.send('agents:attention-requested', { sessionId: pendingAttentionSessionId });
316
+ pendingAttentionSessionId = '';
317
+ } catch (error) {
318
+ reportRecoverableError('ipc-send:agents:attention-requested', error);
319
+ }
320
+ }
321
+
322
+ function markRendererReady() {
323
+ rendererBootstrapped = true;
324
+ if (!pendingAttentionSessionId || !mainWindow || mainWindow.isDestroyed()) return { ok: true };
325
+ const sessionId = pendingAttentionSessionId;
274
326
  try {
275
- mainWindow.webContents.send('agents:attention-requested', { sessionId: String(session && session.id || '') });
327
+ mainWindow.webContents.send('agents:attention-requested', { sessionId });
328
+ pendingAttentionSessionId = '';
276
329
  } catch (error) {
277
330
  reportRecoverableError('ipc-send:agents:attention-requested', error);
278
331
  }
332
+ return { ok: true };
279
333
  }
280
334
 
281
335
  function createAttentionNotifier() {
@@ -310,10 +364,46 @@ function installationType() {
310
364
  return fs.existsSync(path.join(__dirname, '.git')) ? 'source' : 'npm';
311
365
  }
312
366
 
313
- function setupRuntime() {
367
+ async function installDownloadedUpdate() {
368
+ if (!updateManager) throw new Error('업데이트 관리자가 준비되지 않았습니다.');
369
+ const downloaded = await updateManager.download();
370
+ const outcome = await launchDownloadedUpdate({
371
+ platform: process.platform,
372
+ installType: process.env.PORTABLE_EXECUTABLE_FILE ? 'portable' : installationType(),
373
+ installerPath: downloaded.downloadedPath,
374
+ downloadsDir: path.join(app.getPath('userData'), 'updates'),
375
+ appPath: process.execPath,
376
+ parentPid: process.pid,
377
+ shell,
378
+ });
379
+ if (outcome.mode === 'automatic') {
380
+ isQuitting = true;
381
+ setImmediate(() => app.quit());
382
+ }
383
+ return { ...updateManager.getState(), installMode: outcome.mode };
384
+ }
385
+
386
+ async function setupRuntime() {
387
+ loadProviderVisibility();
314
388
  const runsDir = userFile('agent-runs');
315
389
  runner = new AgentRunner({ runsDir });
316
- terminalManager = new TerminalManager();
390
+ const terminalStoreFile = userFile('terminal-sessions.json');
391
+ const terminalHostFile = userFile('terminal-host.json');
392
+ terminalManager = demoCapture
393
+ ? new TerminalManager({
394
+ storeFile: terminalStoreFile,
395
+ onPersistenceError: (operation, error) => reportRecoverableError(`terminal-sessions:${operation}`, error),
396
+ })
397
+ : new TerminalHostClient({
398
+ discoveryFile: terminalHostFile,
399
+ spawnHost: () => launchTerminalHost({
400
+ executable: resolveTerminalHostExecutable({ isPackaged: app.isPackaged }),
401
+ script: path.join(__dirname, 'src', 'terminalHostDaemon.js'),
402
+ storeFile: terminalStoreFile,
403
+ discoveryFile: terminalHostFile,
404
+ bridgeHome,
405
+ }),
406
+ });
317
407
  updateManager = new UpdateManager({
318
408
  currentVersion: app.getVersion(),
319
409
  platform: process.platform,
@@ -338,12 +428,29 @@ function setupRuntime() {
338
428
  }
339
429
  terminalManager.on('data', payload => sendTerminal('terminals:data', payload));
340
430
  terminalManager.on('state', payload => {
341
- sendTerminal('terminals:state', payload);
431
+ if (!payload.session || payload.session.type !== 'agent' || isProviderVisible(payload.session.provider)) {
432
+ sendTerminal('terminals:state', { ...payload, sessions: visibleTerminalSessions(payload.sessions) });
433
+ }
434
+ updateBackgroundTrayMenu();
435
+ if (monitorWorker) monitorWorker.postMessage({ type: 'bridge-presence', bridges: bridgePresence() });
436
+ });
437
+ terminalManager.on('disconnect', () => {
438
+ sendTerminal('terminals:connection', { state: 'reconnecting', message: mainText('terminalHostReconnecting') });
439
+ });
440
+ terminalManager.on('reconnect', payload => {
441
+ const sessions = visibleTerminalSessions(payload?.sessions || terminalManager.list());
442
+ sendTerminal('terminals:state', { change: 'reconnected', session: null, sessions });
443
+ sendTerminal('terminals:connection', { state: 'connected', message: mainText('terminalHostReconnected') });
342
444
  updateBackgroundTrayMenu();
343
445
  if (monitorWorker) monitorWorker.postMessage({ type: 'bridge-presence', bridges: bridgePresence() });
344
446
  });
345
- bridgeServer = new BridgeServer({ terminalManager, home: bridgeHome, platform: process.platform });
346
- bridgeServer.start().catch(error => sendTerminal('terminals:error', { id: '', message: `외부 터미널 브리지를 열지 못했습니다: ${error.message}` }));
447
+ terminalManager.on('reconnect-error', error => {
448
+ sendTerminal('terminals:connection', {
449
+ state: 'failed',
450
+ message: mainText('terminalHostReconnectFailed', { reason: error?.message || String(error) }),
451
+ });
452
+ });
453
+ await terminalManager.connect();
347
454
  availability = probeProviders();
348
455
  monitorWorker = new Worker(path.join(__dirname, 'src', 'monitorWorker.js'), {
349
456
  workerData: { runsDir, home: os.homedir(), intervalMs: 1200, availability },
@@ -352,7 +459,7 @@ function setupRuntime() {
352
459
  monitorWorker.on('message', message => {
353
460
  if (message && message.type === 'snapshot') {
354
461
  lastSnapshot = message.snapshot;
355
- attentionNotifier.sync(lastSnapshot);
462
+ attentionNotifier.sync(visibleSnapshotSessions(lastSnapshot));
356
463
  sendSnapshot(lastSnapshot);
357
464
  }
358
465
  if (message && message.type === 'detail-result') {
@@ -373,10 +480,11 @@ function bridgePresence() {
373
480
  if (!terminalManager) return [];
374
481
  const environment = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'macos' : 'linux');
375
482
  return terminalManager.list()
376
- .filter(session => session.type === 'agent' && session.status === 'running')
483
+ .filter(session => session.type === 'agent' && (session.status === 'running' || session.status === 'starting'))
377
484
  .map(session => ({
378
485
  id: session.bridgeId || session.id,
379
486
  bridgeId: session.bridgeId || '',
487
+ linkedSessionId: session.bridgeId || '',
380
488
  terminalId: session.id,
381
489
  provider: session.provider,
382
490
  pid: session.pid,
@@ -401,17 +509,20 @@ function bootstrapState() {
401
509
  id: process.platform,
402
510
  label: process.platform === 'darwin' ? 'macOS' : (process.platform === 'win32' ? 'Windows' : 'Linux'),
403
511
  localShell: process.platform === 'win32' ? 'powershell' : 'shell',
404
- localShellLabel: process.platform === 'darwin' ? 'Mac 명령창' : (process.platform === 'win32' ? 'Windows 명령창' : 'Linux 명령창'),
512
+ localShellLabel: process.platform === 'darwin' ? 'macOS shell' : (process.platform === 'win32' ? 'Windows PowerShell' : 'Linux shell'),
405
513
  nativeTmux: process.platform !== 'win32',
406
514
  },
407
515
  bridgeCli: bridgeLauncher,
408
516
  update: updateManager ? updateManager.getState() : null,
517
+ providerVisibility: providerVisibilityStore ? providerVisibilityStore.snapshot() : { hidden: [] },
409
518
  };
410
519
  }
411
520
 
412
521
  function requestAgentDetail(sessionId) {
413
522
  return new Promise(resolve => {
414
523
  if (!monitorWorker || String(sessionId || '').length > 500) return resolve(null);
524
+ const card = (lastSnapshot.sessions || []).find(session => session.id === String(sessionId || ''));
525
+ if (card && !isProviderVisible(card.provider)) return resolve(null);
415
526
  const requestId = ++detailRequestId;
416
527
  const timer = setTimeout(() => {
417
528
  if (!pendingDetails.has(requestId)) return;
@@ -432,9 +543,10 @@ function registerIpcHandlers() {
432
543
  registerAppIpc({
433
544
  handleTrusted,
434
545
  bootstrap: bootstrapState,
546
+ rendererReady: markRendererReady,
435
547
  backgroundState: () => ({
436
548
  visible: Boolean(mainWindow && !mainWindow.isDestroyed() && mainWindow.isVisible()),
437
- backgroundSessions: backgroundAgentSessions().length,
549
+ backgroundSessions: backgroundTerminalSessions().length,
438
550
  trayReady: Boolean(backgroundTray),
439
551
  }),
440
552
  show: () => { showMainWindow(); return { ok: true }; },
@@ -443,13 +555,16 @@ function registerIpcHandlers() {
443
555
  updateBackgroundTrayMenu();
444
556
  return { locale: appLocale };
445
557
  },
558
+ setProviderVisibility: saveProviderVisibility,
446
559
  updateManager: () => updateManager,
560
+ installUpdate: installDownloadedUpdate,
447
561
  });
448
562
  registerAgentIpc({
449
563
  handleTrusted,
450
564
  snapshot: () => { refreshMonitor(); return lastSnapshot; },
451
565
  requestDetail: requestAgentDetail,
452
566
  runner: () => runner,
567
+ isProviderVisible,
453
568
  probeProviders: () => {
454
569
  availability = probeProviders();
455
570
  if (monitorWorker) monitorWorker.postMessage({ type: 'availability', availability });
@@ -462,6 +577,7 @@ function registerIpcHandlers() {
462
577
  requireTrustedSender,
463
578
  trustedSender,
464
579
  manager: () => terminalManager,
580
+ isProviderVisible,
465
581
  listWslDistros,
466
582
  sendError: payload => sendTerminal('terminals:error', payload),
467
583
  });
@@ -514,11 +630,14 @@ function registerIpcHandlers() {
514
630
 
515
631
  registerIpcHandlers();
516
632
 
517
- app.whenReady().then(() => {
633
+ app.whenReady().then(async () => {
518
634
  hydratePlatformPath();
519
- setupRuntime();
635
+ await setupRuntime();
520
636
  createWindow();
521
637
  app.on('activate', showMainWindow);
638
+ }).catch(error => {
639
+ dialog.showErrorBox('LoadToAgent 시작 실패', error.stack || error.message || String(error));
640
+ app.quit();
522
641
  });
523
642
 
524
643
  app.on('window-all-closed', () => {
@@ -528,8 +647,8 @@ app.on('window-all-closed', () => {
528
647
  app.on('before-quit', () => {
529
648
  isQuitting = true;
530
649
  if (attentionNotifier) attentionNotifier.dispose();
531
- if (bridgeServer) bridgeServer.dispose();
532
- if (terminalManager) terminalManager.dispose();
650
+ if (terminalManager instanceof TerminalHostClient) terminalManager.dispose({ shutdownIfIdle: true });
651
+ else if (terminalManager) terminalManager.dispose({ preserveSessions: true });
533
652
  if (monitorWorker) {
534
653
  monitorWorker.postMessage({ type: 'stop' });
535
654
  monitorWorker.terminate();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadtoagent",
3
- "version": "1.1.0",
3
+ "version": "1.3.2",
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",
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,6 +96,7 @@
90
96
  "bin/**/*",
91
97
  "node_modules/node-pty/**/*"
92
98
  ],
99
+ "afterPack": "scripts/after-pack.js",
93
100
  "win": {
94
101
  "icon": "build/icon.ico",
95
102
  "target": [
@@ -123,13 +130,15 @@
123
130
  "artifactName": "LoadToAgent-${version}-${arch}.${ext}"
124
131
  },
125
132
  "portable": {
126
- "artifactName": "LoadToAgent-${version}-portable.exe"
133
+ "artifactName": "LoadToAgent-${version}-portable.exe",
134
+ "unpackDirName": false
127
135
  },
128
136
  "nsis": {
129
137
  "artifactName": "LoadToAgent-Setup-${version}.exe",
130
138
  "oneClick": false,
131
139
  "perMachine": false,
132
- "allowToChangeInstallationDirectory": true
140
+ "allowToChangeInstallationDirectory": true,
141
+ "runAfterFinish": false
133
142
  }
134
143
  }
135
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'),
@@ -29,9 +35,9 @@ contextBridge.exposeInMainWorld('loadtoagent', {
29
35
  wslDistros: () => ipcRenderer.invoke('wsl:list-distros'),
30
36
  terminalGet: id => ipcRenderer.invoke('terminals:get', id),
31
37
  terminalCreate: options => ipcRenderer.invoke('terminals:create', options),
32
- terminalWrite: (id, data) => ipcRenderer.send('terminals:write', id, data),
38
+ terminalWrite: (id, data) => ipcRenderer.invoke('terminals:write', id, data),
33
39
  terminalCommand: (id, command) => ipcRenderer.invoke('terminals:command', id, command),
34
- terminalResize: (id, cols, rows) => ipcRenderer.send('terminals:resize', id, cols, rows),
40
+ terminalResize: (id, cols, rows) => ipcRenderer.invoke('terminals:resize', id, cols, rows),
35
41
  terminalSignal: (id, signal) => ipcRenderer.invoke('terminals:signal', id, signal),
36
42
  terminalRestart: id => ipcRenderer.invoke('terminals:restart', id),
37
43
  terminalClose: id => ipcRenderer.invoke('terminals:close', id),
@@ -62,6 +68,11 @@ contextBridge.exposeInMainWorld('loadtoagent', {
62
68
  ipcRenderer.on('terminals:error', handler);
63
69
  return () => ipcRenderer.removeListener('terminals:error', handler);
64
70
  },
71
+ onTerminalConnection: callback => {
72
+ const handler = (_event, payload) => callback(payload);
73
+ ipcRenderer.on('terminals:connection', handler);
74
+ return () => ipcRenderer.removeListener('terminals:connection', handler);
75
+ },
65
76
  onSnapshot: callback => {
66
77
  const handler = (_event, snapshot) => callback(snapshot);
67
78
  ipcRenderer.on('agents:snapshot', handler);