loadtoagent 1.3.3 → 1.3.4

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 (36) hide show
  1. package/main.js +53 -8
  2. package/package.json +2 -1
  3. package/renderer/app-agent-actions.js +172 -66
  4. package/renderer/app-bootstrap.js +3 -1
  5. package/renderer/app-dashboard.js +36 -7
  6. package/renderer/app-drawer-content.js +177 -59
  7. package/renderer/app-drawer-data.js +7 -3
  8. package/renderer/app-drawer.js +72 -33
  9. package/renderer/app-events-dialogs.js +8 -1
  10. package/renderer/app-events-navigation.js +7 -0
  11. package/renderer/app-events-sessions.js +136 -13
  12. package/renderer/app-graph-model.js +2 -5
  13. package/renderer/app-graph-orchestration.js +7 -18
  14. package/renderer/app-graph-view.js +194 -48
  15. package/renderer/app-management.js +330 -35
  16. package/renderer/app-quality.js +5 -0
  17. package/renderer/app-session-render.js +20 -83
  18. package/renderer/app.js +5 -1
  19. package/renderer/i18n-messages.js +232 -21
  20. package/renderer/index.html +9 -6
  21. package/renderer/styles-components.css +140 -0
  22. package/renderer/styles-control-room.css +1439 -0
  23. package/renderer/styles-management.css +390 -3
  24. package/renderer/styles-responsive-shell.css +5 -0
  25. package/renderer/styles-runtime-overview.css +20 -0
  26. package/renderer/terminal-agent.js +31 -7
  27. package/renderer/terminal.js +4 -1
  28. package/src/agentMonitor/claudeParser.js +8 -4
  29. package/src/agentMonitor/codexParser.js +5 -4
  30. package/src/agentMonitor/genericParser.js +7 -6
  31. package/src/agentMonitor.js +44 -4
  32. package/src/attentionNotifier.js +3 -0
  33. package/src/ipc/registerTerminalIpc.js +2 -2
  34. package/src/monitorWorker.js +1 -1
  35. package/src/processMonitor.js +68 -7
  36. package/src/terminalManager.js +16 -2
@@ -60,19 +60,20 @@ function createGenericParser(dependencies) {
60
60
  return out;
61
61
  }
62
62
 
63
- function readSessionFile(fileInfo) {
63
+ function readSessionFile(fileInfo, options = {}) {
64
64
  const isJsonl = /\.jsonl$/i.test(fileInfo.file);
65
65
  return isJsonl
66
- ? readJsonLines(fileInfo.file)
66
+ ? readJsonLines(fileInfo.file, options.fullHistory ? Math.max(1, Number(fileInfo.size || 0) + 1) : undefined)
67
67
  : { rows: [readJson(fileInfo.file, {})], truncated: false };
68
68
  }
69
69
 
70
- function initializeSession(fileInfo, provider, parsed) {
70
+ function initializeSession(fileInfo, provider, parsed, options = {}) {
71
71
  const rows = parsed.rows.filter(Boolean);
72
72
  const root = rows.length === 1 ? rows[0] : { events: rows };
73
73
  const externalId = root.session_id || root.sessionId || root.id
74
74
  || path.basename(fileInfo.file).replace(/\.(jsonl?|ndjson)$/i, '');
75
75
  const session = baseSession(provider, externalId, fileInfo.file, fileInfo);
76
+ session.fullHistory = Boolean(options.fullHistory);
76
77
  session.truncated = parsed.truncated;
77
78
  session.cwd = root.cwd || root.projectPath || root.project_path || '';
78
79
  session.originCwd = session.cwd;
@@ -264,9 +265,9 @@ function createGenericParser(dependencies) {
264
265
  return session;
265
266
  }
266
267
 
267
- return function parseGeneric(fileInfo, provider) {
268
- const parsed = readSessionFile(fileInfo);
269
- const initialized = initializeSession(fileInfo, provider, parsed);
268
+ return function parseGeneric(fileInfo, provider, options = {}) {
269
+ const parsed = readSessionFile(fileInfo, options);
270
+ const initialized = initializeSession(fileInfo, provider, parsed, options);
270
271
  if (!initialized.rows.length) return null;
271
272
  const events = initialized.rows.length === 1 && Array.isArray(initialized.root.events)
272
273
  ? initialized.root.events
@@ -56,7 +56,7 @@ function timestamp(value, fallback = null) {
56
56
  }
57
57
 
58
58
  function addMessage(session, message) {
59
- const text = compactText(message.text, message.type === 'tool' ? 1600 : 6000);
59
+ const text = compactText(message.text, session.fullHistory ? Number.MAX_SAFE_INTEGER : (message.type === 'tool' ? 1600 : 6000));
60
60
  if (!text && message.type !== 'tool') return;
61
61
  const row = {
62
62
  id: String(message.id || `${session.id}:m:${session.messages.length}`),
@@ -353,6 +353,13 @@ function contextInfo(used, windowInfo) {
353
353
  }
354
354
 
355
355
  function trimSession(session) {
356
+ if (session.fullHistory) {
357
+ session.omittedMessages = 0;
358
+ session.omittedLifecycle = 0;
359
+ session.truncated = false;
360
+ if (!session.messages.length) addMessage(session, { id: `${session.id}:empty`, role: 'system', text: '표시할 대화 메시지가 아직 없습니다.', timestamp: session.updatedAt });
361
+ return;
362
+ }
356
363
  session.omittedMessages = Math.max(0, session.messages.length - MAX_MESSAGES);
357
364
  session.omittedLifecycle = Math.max(0, session.lifecycle.length - MAX_LIFECYCLE);
358
365
  session.messages = session.messages.slice(-MAX_MESSAGES);
@@ -360,7 +367,7 @@ function trimSession(session) {
360
367
  if (!session.messages.length) addMessage(session, { id: `${session.id}:empty`, role: 'system', text: '표시할 대화 메시지가 아직 없습니다.', timestamp: session.updatedAt });
361
368
  }
362
369
 
363
- function parseManagedSession(runDir) {
370
+ function parseManagedSession(runDir, options = {}) {
364
371
  const meta = readJson(path.join(runDir, 'meta.json'));
365
372
  const live = readJson(path.join(runDir, 'session.json'));
366
373
  if (!meta || !live) return null;
@@ -373,6 +380,7 @@ function parseManagedSession(runDir) {
373
380
  source: 'loadtoagent',
374
381
  sourceLabel: 'LoadToAgent 실행',
375
382
  statusObserved: true,
383
+ fullHistory: Boolean(options.fullHistory),
376
384
  };
377
385
  session.originCwd = session.originCwd || meta.cwd || session.cwd || '';
378
386
  session.usage = finalizeUsage(session.usage);
@@ -482,8 +490,8 @@ class AgentMonitor extends EventEmitter {
482
490
  }).filter(Boolean).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, max);
483
491
  }
484
492
 
485
- parseFile(info, parser) {
486
- const key = `${info.file}|${info.mtimeMs}|${info.size}`;
493
+ parseFile(info, parser, variant = '') {
494
+ const key = `${info.file}|${info.mtimeMs}|${info.size}|${variant}`;
487
495
  const cached = this.parseCache.get(key);
488
496
  if (cached) return cached;
489
497
  const value = parser(info);
@@ -495,6 +503,38 @@ class AgentMonitor extends EventEmitter {
495
503
  return value;
496
504
  }
497
505
 
506
+ detailSession(sessionId) {
507
+ const stored = (this.lastSnapshot.sessions || []).find(session => session.id === String(sessionId || '')) || null;
508
+ if (!stored) return null;
509
+ let detailed = null;
510
+ if (stored.source === 'loadtoagent' && stored.runId && this.runsDir) {
511
+ detailed = parseManagedSession(path.join(this.runsDir, stored.runId), { fullHistory: true });
512
+ } else if (stored.file) {
513
+ const stat = safeStat(stored.file);
514
+ if (stat && stat.isFile()) {
515
+ const info = { file: stored.file, mtimeMs: stat.mtimeMs, size: stat.size };
516
+ const parser = stored.provider === 'claude'
517
+ ? item => parseClaude(item, { fullHistory: true })
518
+ : stored.provider === 'codex'
519
+ ? item => parseCodex(item, { fullHistory: true })
520
+ : item => parseGeneric(item, stored.provider, { fullHistory: true });
521
+ detailed = this.parseFile(info, parser, 'full-history');
522
+ }
523
+ }
524
+ if (!detailed) return stored;
525
+ return {
526
+ ...stored,
527
+ ...detailed,
528
+ environment: stored.environment,
529
+ delegation: stored.delegation || detailed.delegation,
530
+ childIds: stored.childIds || detailed.childIds || [],
531
+ fullHistory: true,
532
+ truncated: false,
533
+ omittedMessages: 0,
534
+ omittedLifecycle: 0,
535
+ };
536
+ }
537
+
498
538
  hintedFiles(paths, max) {
499
539
  return (paths || []).map(value => {
500
540
  if (value && typeof value === 'object' && value.file && Number.isFinite(value.mtimeMs) && Number.isFinite(value.size)) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  class AttentionNotifier {
4
4
  constructor(options = {}) {
5
+ this.enabled = options.enabled !== false;
5
6
  this.Notification = options.Notification;
6
7
  this.isSupported = options.isSupported || (() => Boolean(this.Notification));
7
8
  this.copy = options.copy || (() => ({ title: '내 확인이 필요합니다', body: '응답이나 선택을 기다리는 AI 세션이 있습니다.' }));
@@ -12,6 +13,7 @@ class AttentionNotifier {
12
13
  }
13
14
 
14
15
  sync(snapshot) {
16
+ if (!this.enabled) return [];
15
17
  const needsAttention = (snapshot && Array.isArray(snapshot.sessions) ? snapshot.sessions : [])
16
18
  .filter(session => session && session.id && (
17
19
  session.attention?.required
@@ -32,6 +34,7 @@ class AttentionNotifier {
32
34
  }
33
35
 
34
36
  notify(session) {
37
+ if (!this.enabled) return null;
35
38
  let supported = false;
36
39
  try {
37
40
  supported = Boolean(this.Notification && this.isSupported());
@@ -3,7 +3,7 @@
3
3
  function registerTerminalIpc({ ipcMain, requireTrustedSender, trustedSender, manager, isProviderVisible = () => true, listWslDistros, sendError }) {
4
4
  ipcMain.handle('terminals:list', event => {
5
5
  requireTrustedSender(event);
6
- return manager() ? manager().list().filter(session => session.type !== 'agent' || isProviderVisible(session.provider)) : [];
6
+ return manager() ? manager().list().filter(session => !session.transient && (session.type !== 'agent' || isProviderVisible(session.provider))) : [];
7
7
  });
8
8
  ipcMain.handle('wsl:list-distros', event => {
9
9
  requireTrustedSender(event);
@@ -12,7 +12,7 @@ function registerTerminalIpc({ ipcMain, requireTrustedSender, trustedSender, man
12
12
  ipcMain.handle('terminals:get', async (event, id) => {
13
13
  requireTrustedSender(event);
14
14
  const session = manager() ? await manager().get(id, true) : null;
15
- return session && session.type === 'agent' && !isProviderVisible(session.provider) ? null : session;
15
+ return session && (session.transient || (session.type === 'agent' && !isProviderVisible(session.provider))) ? null : session;
16
16
  });
17
17
  ipcMain.handle('terminals:create', (event, options) => {
18
18
  requireTrustedSender(event);
@@ -317,7 +317,7 @@ parentPort.on('message', message => {
317
317
  }
318
318
  if (message.type === 'detail') {
319
319
  const runtime = lastPublishedSessions.find(item => item.id === message.sessionId) || null;
320
- const stored = (monitor.lastSnapshot.sessions || []).find(item => item.id === message.sessionId) || null;
320
+ const stored = monitor.detailSession(message.sessionId);
321
321
  const merged = stored && runtime
322
322
  ? { ...stored, status: runtime.status, statusDetail: runtime.statusDetail, statusObserved: runtime.statusObserved, runtimePresence: runtime.runtimePresence || [] }
323
323
  : (stored || runtime);
@@ -5,6 +5,18 @@ const { blankUsage } = require('./providerRegistry');
5
5
 
6
6
  const DEFAULT_SCAN_TTL_MS = 12_000;
7
7
  const WMIC_QUERY = "Name='claude.exe' or Name='codex.exe' or Name='node.exe' or Name='gemini.exe' or Name='grok.exe'";
8
+ const WINDOWS_PROCESS_SCRIPT = [
9
+ "$ProgressPreference = 'SilentlyContinue'",
10
+ "$ErrorActionPreference = 'Stop'",
11
+ '[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)',
12
+ "$names = @('claude.exe','codex.exe','node.exe','gemini.exe','grok.exe')",
13
+ '$rows = Get-CimInstance Win32_Process | Where-Object { $names -contains $_.Name } | ForEach-Object {',
14
+ " $started = if ($_.CreationDate) { $_.CreationDate.ToUniversalTime().ToString('o') } else { $null }",
15
+ ' [pscustomobject]@{ pid = [int]$_.ProcessId; parentPid = [int]$_.ParentProcessId; name = [string]$_.Name; commandLine = [string]$_.CommandLine; startedAt = $started }',
16
+ '}',
17
+ '@($rows) | ConvertTo-Json -Compress',
18
+ ].join('; ');
19
+ const WINDOWS_PROCESS_SCRIPT_BASE64 = Buffer.from(WINDOWS_PROCESS_SCRIPT, 'utf16le').toString('base64');
8
20
 
9
21
  function parseCsvRows(value) {
10
22
  const text = Buffer.isBuffer(value) ? value.toString('utf8') : String(value || '');
@@ -131,6 +143,42 @@ function processRows(value) {
131
143
  })).filter(item => item.pid > 0);
132
144
  }
133
145
 
146
+ function powershellProcessRows(value) {
147
+ const text = (Buffer.isBuffer(value) ? value.toString('utf8') : String(value || '')).replace(/^\uFEFF/, '').trim();
148
+ if (!text) return [];
149
+ const parsed = JSON.parse(text);
150
+ return (Array.isArray(parsed) ? parsed : [parsed]).map(row => {
151
+ const timestamp = Date.parse(row.startedAt || '');
152
+ return {
153
+ pid: Number(row.pid || 0),
154
+ parentPid: Number(row.parentPid || 0),
155
+ name: String(row.name || ''),
156
+ commandLine: String(row.commandLine || ''),
157
+ startedAt: Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null,
158
+ };
159
+ }).filter(item => item.pid > 0);
160
+ }
161
+
162
+ function windowsProcessRows(run = execFileSync) {
163
+ try {
164
+ const output = run('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', WINDOWS_PROCESS_SCRIPT_BASE64], {
165
+ encoding: 'utf8', windowsHide: true, timeout: 8_000, maxBuffer: 4 * 1024 * 1024,
166
+ });
167
+ return powershellProcessRows(output);
168
+ } catch (powershellError) {
169
+ try {
170
+ const output = run('wmic.exe', ['process', 'where', WMIC_QUERY, 'get', 'ProcessId,ParentProcessId,CreationDate,Name,CommandLine', '/format:csv'], {
171
+ encoding: 'utf8', windowsHide: true, timeout: 8_000, maxBuffer: 2 * 1024 * 1024,
172
+ });
173
+ return processRows(output);
174
+ } catch (wmicError) {
175
+ const error = new Error(`Windows 프로세스 조회 실패: ${powershellError.message || powershellError}; ${wmicError.message || wmicError}`);
176
+ error.cause = powershellError;
177
+ throw error;
178
+ }
179
+ }
180
+ }
181
+
134
182
  function commandArgument(commandLine, name) {
135
183
  const escaped = String(name || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
136
184
  const match = String(commandLine || '').match(new RegExp(`(?:^|\\s)${escaped}\\s+(?:"([^"]+)"|'([^']+)'|(\\S+))`, 'i'));
@@ -151,6 +199,14 @@ function processSessionExternalId(processInfo = {}, provider = '') {
151
199
  return '';
152
200
  }
153
201
 
202
+ function processInteractionMode(processInfo = {}, provider = '') {
203
+ const commandLine = String(processInfo.commandLine || processInfo.CommandLine || '');
204
+ if (provider === 'claude' && /(?:^|\s)(?:-p|--print)(?:\s|$)/i.test(commandLine)) return 'batch';
205
+ if (provider === 'codex' && /(?:^|\s)exec(?:\s|$)/i.test(commandLine)) return 'batch';
206
+ if (provider === 'gemini' && /(?:^|\s)(?:-p|--prompt)(?:\s|$)/i.test(commandLine)) return 'batch';
207
+ return 'interactive';
208
+ }
209
+
154
210
  function pathlessSessionId(value) {
155
211
  const text = String(value || '').trim().replace(/^"|"$/g, '');
156
212
  if (!text) return '';
@@ -192,14 +248,13 @@ function selectAgentProcesses(rows, options = {}) {
192
248
  command: String(item.name || '').replace(/\.exe$/i, '').split(/[\\/]/).pop(),
193
249
  startedAt: item.startedAt,
194
250
  externalId: processSessionExternalId(item, item.provider),
251
+ interactionMode: processInteractionMode(item, item.provider),
195
252
  })).sort((a, b) => a.provider.localeCompare(b.provider) || a.pid - b.pid);
196
253
  }
197
254
 
198
255
  function utilityProcess(processInfo = {}) {
199
256
  const commandLine = String(processInfo.commandLine || processInfo.args || '');
200
- return /Extract durable memory candidates from this Claude Code transcript tail/i.test(commandLine)
201
- || /Reply with exactly OK\. Do not use tools\.?/i.test(commandLine)
202
- || /You are a memory extraction/i.test(commandLine);
257
+ return /(?:^|\s)(?:-p|--print)\s+["']?(?:Extract durable memory candidates from this Claude Code transcript tail|Reply with exactly OK\. Do not use tools\.?|You are a memory extraction)/i.test(commandLine);
203
258
  }
204
259
 
205
260
  function utilitySession(session) {
@@ -230,6 +285,12 @@ function markRuntime(session, presence) {
230
285
  const existing = Array.isArray(session.runtimePresence) ? session.runtimePresence : [];
231
286
  if (!existing.some(item => item.id === presence.id)) existing.push(presence);
232
287
  session.runtimePresence = existing;
288
+ if (presence.interactionMode === 'batch') {
289
+ session.conversationStatus = session.status;
290
+ session.status = 'running';
291
+ session.statusDetail = `백그라운드 자율 실행 중 · PID ${presence.pid || '--'}`;
292
+ session.statusObserved = true;
293
+ }
233
294
  return session;
234
295
  }
235
296
 
@@ -417,10 +478,7 @@ class ProcessMonitor {
417
478
  try {
418
479
  let processes;
419
480
  if (this.platform === 'win32') {
420
- const output = this.execFileSync('wmic.exe', ['process', 'where', WMIC_QUERY, 'get', 'ProcessId,ParentProcessId,CreationDate,Name,CommandLine', '/format:csv'], {
421
- encoding: 'utf8', windowsHide: true, timeout: 8_000, maxBuffer: 2 * 1024 * 1024,
422
- });
423
- processes = selectAgentProcesses(processRows(output));
481
+ processes = selectAgentProcesses(windowsProcessRows(this.execFileSync));
424
482
  } else {
425
483
  const output = this.execFileSync('ps', ['-axo', 'pid=,ppid=,etime=,comm=,args='], { encoding: 'utf8', timeout: 8_000, maxBuffer: 2 * 1024 * 1024 });
426
484
  const environment = this.platform === 'darwin' ? 'macos' : 'linux';
@@ -438,6 +496,8 @@ module.exports = {
438
496
  ProcessMonitor,
439
497
  parseCsvRows,
440
498
  processRows,
499
+ powershellProcessRows,
500
+ windowsProcessRows,
441
501
  wmiDateToIso,
442
502
  providerFromWindowsProcess,
443
503
  providerFromPosixProcess,
@@ -445,6 +505,7 @@ module.exports = {
445
505
  elapsedSeconds,
446
506
  selectAgentProcesses,
447
507
  processSessionExternalId,
508
+ processInteractionMode,
448
509
  utilityProcess,
449
510
  runtimeLinkScore,
450
511
  bridgeLinkScore,
@@ -40,7 +40,7 @@ function terminalEnvironment(extra = {}) {
40
40
  for (const [key, value] of Object.entries({ ...process.env, ...extra })) {
41
41
  if (value != null) env[key] = String(value);
42
42
  }
43
- env.TERM = env.TERM || 'xterm-256color';
43
+ env.TERM = !env.TERM || String(env.TERM).toLowerCase() === 'dumb' ? 'xterm-256color' : env.TERM;
44
44
  env.COLORTERM = env.COLORTERM || 'truecolor';
45
45
  return env;
46
46
  }
@@ -142,6 +142,7 @@ function normalizeLaunchOptions(options = {}, platform = process.platform) {
142
142
  args,
143
143
  bridgeId: cleanText(options.bridgeId, 100),
144
144
  title: cleanText(options.title, 100),
145
+ transient: Boolean(options.transient),
145
146
  cols: numericDimension(options.cols, 120, 20, 500),
146
147
  rows: numericDimension(options.rows, 32, 5, 200),
147
148
  };
@@ -207,6 +208,7 @@ function publicSession(session, includeReplay = false) {
207
208
  tmuxPane: session.options.tmuxPane,
208
209
  provider: session.options.provider,
209
210
  bridgeId: session.options.bridgeId,
211
+ transient: Boolean(session.options.transient),
210
212
  background: session.options.type === 'agent',
211
213
  recoveredAfterHostRestart: Boolean(session.recoveredAfterHostRestart),
212
214
  recoverySkippedReason: session.recoverySkippedReason || '',
@@ -243,6 +245,7 @@ function restoredOptions(value = {}, platform = process.platform) {
243
245
  args: Array.isArray(value.args) ? value.args.slice(0, 80).map(item => cleanText(item, 2_000)) : [],
244
246
  bridgeId: cleanText(value.bridgeId, 100),
245
247
  title: cleanText(value.title, 100),
248
+ transient: Boolean(value.transient),
246
249
  cols: numericDimension(value.cols, 120, 20, 500),
247
250
  rows: numericDimension(value.rows, 32, 5, 200),
248
251
  };
@@ -356,7 +359,7 @@ class TerminalManager extends EventEmitter {
356
359
  this.fileSystem.mkdirSync(path.dirname(this.storeFile), { recursive: true });
357
360
  const payload = {
358
361
  version: STORE_VERSION,
359
- sessions: [...this.sessions.values()].map(persistedSession),
362
+ sessions: [...this.sessions.values()].filter(session => !session.options.transient).map(persistedSession),
360
363
  };
361
364
  this.fileSystem.writeFileSync(temporary, JSON.stringify(payload), 'utf8');
362
365
  this.fileSystem.renameSync(temporary, this.storeFile);
@@ -376,6 +379,11 @@ class TerminalManager extends EventEmitter {
376
379
  for (const session of this.sessions.values()) {
377
380
  if (!session.recoveryPending) continue;
378
381
  session.recoveryPending = false;
382
+ if (session.options.type === 'agent'
383
+ && /TERM is set to ["']?dumb["']?[\s\S]{0,500}Continue anyway\?/i.test(session.replay)) {
384
+ this.sessions.delete(session.id);
385
+ continue;
386
+ }
379
387
  if (!hasSafeAgentResume(session.options)) {
380
388
  session.status = 'exited';
381
389
  session.pid = null;
@@ -485,6 +493,12 @@ class TerminalManager extends EventEmitter {
485
493
  session.exitCode = Number.isFinite(event.exitCode) ? event.exitCode : null;
486
494
  session.signal = Number.isFinite(event.signal) ? event.signal : null;
487
495
  session.updatedAt = new Date().toISOString();
496
+ if (session.options.transient) {
497
+ this.sessions.delete(session.id);
498
+ this.emit('state', { change: 'removed', session: publicSession(session, false), sessions: this.list() });
499
+ this.persistNow();
500
+ return;
501
+ }
488
502
  this.persistNow();
489
503
  this.emitState('updated', session);
490
504
  });