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
@@ -6,7 +6,9 @@ const path = require('path');
6
6
  const { AgentMonitor, buildSummary } = require('./agentMonitor');
7
7
  const { TmuxMonitor, linkAgentSessions } = require('./tmuxMonitor');
8
8
  const { ProcessMonitor, applyRuntimePresence } = require('./processMonitor');
9
+ const { scanCodexAutomationHomes } = require('./automationMonitor');
9
10
  const { reportRecoverableError } = require('./diagnostics');
11
+ const { enrichSession } = require('./sessionIntelligence');
10
12
 
11
13
  const tmuxMonitor = new TmuxMonitor();
12
14
  tmuxMonitor.scan();
@@ -130,6 +132,30 @@ function cardCollaboration(value) {
130
132
  };
131
133
  }
132
134
 
135
+ function cardExecutions(value) {
136
+ return (value || []).slice(-120).map(activity => ({
137
+ id: activity.id,
138
+ callId: activity.callId,
139
+ kind: activity.kind,
140
+ mode: activity.mode,
141
+ tool: clip(activity.tool, 80),
142
+ runtime: clip(activity.runtime, 80),
143
+ label: clip(activity.label, 180),
144
+ command: clip(activity.command, 1200),
145
+ cwd: clip(activity.cwd, 360),
146
+ status: activity.status,
147
+ statusDetail: clip(activity.statusDetail, 180),
148
+ output: clip(activity.output, 2400),
149
+ backgroundId: clip(activity.backgroundId, 180),
150
+ backgroundIdType: clip(activity.backgroundIdType, 40),
151
+ exitCode: activity.exitCode == null ? null : Number(activity.exitCode),
152
+ startedAt: activity.startedAt,
153
+ updatedAt: activity.updatedAt,
154
+ completedAt: activity.completedAt,
155
+ source: activity.source,
156
+ }));
157
+ }
158
+
133
159
  function cardSession(session) {
134
160
  return {
135
161
  id: session.id,
@@ -146,6 +172,7 @@ function cardSession(session) {
146
172
  title: clip(session.title, 180),
147
173
  model: session.model,
148
174
  cwd: session.cwd,
175
+ originCwd: session.originCwd || session.cwd,
149
176
  branch: session.branch,
150
177
  workspace: session.workspace,
151
178
  projectless: Boolean(session.projectless),
@@ -179,25 +206,44 @@ function cardSession(session) {
179
206
  context: session.context,
180
207
  childIds: session.childIds,
181
208
  runtimePresence: session.runtimePresence || [],
209
+ loop: session.loop && typeof session.loop === 'object' ? {
210
+ kind: clip(session.loop.kind, 40),
211
+ iteration: Math.max(0, Number(session.loop.iteration || 0)),
212
+ phase: clip(session.loop.phase, 40),
213
+ } : (session.loop === true ? true : null),
182
214
  collaboration: cardCollaboration(session.collaboration),
215
+ executions: cardExecutions(session.executions),
216
+ attention: session.attention,
217
+ progress: session.progress,
218
+ health: session.health,
219
+ controlCapabilities: session.controlCapabilities,
220
+ evidence: session.evidence,
221
+ outcome: session.outcome,
183
222
  messages: selectCardMessages(session.messages),
184
223
  lifecycle: (session.lifecycle || []).slice(-2).map(cardLifecycle),
185
224
  };
186
225
  }
187
226
 
188
- function fingerprint(snapshot, tmux) {
227
+ function fingerprint(snapshot, tmux, automations) {
189
228
  const sessions = snapshot.sessions.map(session => [
190
229
  session.id,
191
230
  session.updatedAt,
192
231
  session.status,
193
232
  session.usage && session.usage.total,
194
233
  session.context && session.context.used,
234
+ session.originCwd,
195
235
  session.workspace,
196
236
  Boolean(session.projectless),
237
+ session.loop && `${session.loop.kind || ''}:${session.loop.iteration || 0}:${session.loop.phase || ''}`,
197
238
  session.childIds && session.childIds.length,
198
239
  session.collaboration && session.collaboration.metrics && Object.values(session.collaboration.metrics).join(':'),
199
240
  session.collaboration && session.collaboration.communications && session.collaboration.communications.length,
200
241
  session.collaboration && session.collaboration.communications && session.collaboration.communications.at(-1) && session.collaboration.communications.at(-1).id,
242
+ (session.executions || []).map(activity => `${activity.id}:${activity.status}:${activity.mode}:${activity.backgroundId || ''}:${activity.updatedAt || ''}`).join(','),
243
+ session.attention && `${session.attention.kind}:${session.attention.required}`,
244
+ session.progress && `${session.progress.stage}:${session.progress.percent}:${session.progress.currentStep}`,
245
+ session.health && `${session.health.level}:${session.health.signals.map(signal => signal.code).join(',')}`,
246
+ session.outcome && `${session.outcome.status}:${session.outcome.artifacts.length}:${session.outcome.checks.length}`,
201
247
  (session.runtimePresence || []).map(item => `${item.id}:${item.pid}:${item.terminalId || ''}`).join(','),
202
248
  ]);
203
249
  const tmuxState = (tmux.distros || []).flatMap(distro => (distro.sessions || []).flatMap(tmuxSession => (tmuxSession.windows || []).flatMap(window => (window.panes || []).map(pane => [
@@ -214,21 +260,32 @@ function fingerprint(snapshot, tmux) {
214
260
  pane.agent && pane.agent.linkedSessionId,
215
261
  pane.agent && pane.agent.updatedAt,
216
262
  ]))));
217
- return JSON.stringify([sessions, tmuxState]);
263
+ const automationState = (automations || []).map(item => [
264
+ item.id, item.name, item.status, item.rrule, item.nextRunAt, item.updatedAt, (item.cwds || []).join('|'),
265
+ ]);
266
+ return JSON.stringify([Math.floor(Date.now() / 60_000), sessions, tmuxState, automationState]);
218
267
  }
219
268
 
220
269
  monitor.on('snapshot', snapshot => {
221
270
  const tmuxBase = tmuxMonitor.scan();
222
- monitor.setHistoryHomes(tmuxMonitor.historyHomes());
271
+ const historyHomes = tmuxMonitor.historyHomes();
272
+ monitor.setHistoryHomes(historyHomes);
223
273
  const tmux = linkAgentSessions(tmuxBase, snapshot.sessions);
224
274
  const processSnapshot = processMonitor.scan();
225
- const sessions = applyRuntimePresence(snapshot.sessions, tmux, processSnapshot, Date.now(), currentBridges);
275
+ const observedSessions = applyRuntimePresence(snapshot.sessions, tmux, processSnapshot, Date.now(), currentBridges);
276
+ const sessions = observedSessions.map(session => enrichSession(session, observedSessions, Date.now()));
277
+ const localKind = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'macos' : 'linux');
278
+ const automations = scanCodexAutomationHomes({
279
+ homes: [{ home: workerData.home, kind: localKind, distro: '', label: 'Local' }, ...historyHomes],
280
+ now: new Date(snapshot.generatedAt),
281
+ });
226
282
  const runtimeSnapshot = {
227
283
  generatedAt: snapshot.generatedAt,
228
284
  sessions,
285
+ automations,
229
286
  summary: buildSummary(sessions, monitor.availability),
230
287
  };
231
- const nextFingerprint = fingerprint(runtimeSnapshot, tmux);
288
+ const nextFingerprint = fingerprint(runtimeSnapshot, tmux, automations);
232
289
  if (nextFingerprint === lastFingerprint) return;
233
290
  lastFingerprint = nextFingerprint;
234
291
  lastPublishedSessions = sessions;
@@ -237,6 +294,7 @@ monitor.on('snapshot', snapshot => {
237
294
  snapshot: {
238
295
  generatedAt: snapshot.generatedAt,
239
296
  sessions: sessions.map(cardSession),
297
+ automations,
240
298
  summary: runtimeSnapshot.summary,
241
299
  tmux,
242
300
  runtime: {
@@ -260,9 +318,10 @@ parentPort.on('message', message => {
260
318
  if (message.type === 'detail') {
261
319
  const runtime = lastPublishedSessions.find(item => item.id === message.sessionId) || null;
262
320
  const stored = (monitor.lastSnapshot.sessions || []).find(item => item.id === message.sessionId) || null;
263
- const session = stored && runtime
321
+ const merged = stored && runtime
264
322
  ? { ...stored, status: runtime.status, statusDetail: runtime.statusDetail, statusObserved: runtime.statusObserved, runtimePresence: runtime.runtimePresence || [] }
265
323
  : (stored || runtime);
324
+ const session = enrichSession(merged, lastPublishedSessions, Date.now());
266
325
  parentPort.postMessage({ type: 'detail-result', requestId: message.requestId, session });
267
326
  }
268
327
  if (message.type === 'stop') {
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function unique(items) {
7
+ return [...new Set(items.filter(Boolean))];
8
+ }
9
+
10
+ function versionParts(value) {
11
+ const match = String(value || '').match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
12
+ return match ? match.slice(1).map(part => Number(part || 0)) : null;
13
+ }
14
+
15
+ function compareVersionNames(left, right) {
16
+ const a = versionParts(left) || [0, 0, 0];
17
+ const b = versionParts(right) || [0, 0, 0];
18
+ for (let index = 0; index < 3; index += 1) {
19
+ if (a[index] !== b[index]) return b[index] - a[index];
20
+ }
21
+ return 0;
22
+ }
23
+
24
+ function resolveNvmDefault(home, fileSystem = fs) {
25
+ const aliasRoot = path.join(home, '.nvm', 'alias');
26
+ let value = 'default';
27
+ const visited = new Set();
28
+ for (let depth = 0; depth < 6; depth += 1) {
29
+ if (/^v?\d+(?:\.\d+){0,2}$/.test(value)) return value.replace(/^v/, '');
30
+ if (value === 'node' || value === 'stable') return '';
31
+ if (!/^[A-Za-z0-9*._/-]+$/.test(value) || value.includes('..') || visited.has(value)) return '';
32
+ visited.add(value);
33
+ try {
34
+ value = String(fileSystem.readFileSync(path.join(aliasRoot, value), 'utf8')).trim();
35
+ } catch (_missingAlias) {
36
+ return '';
37
+ }
38
+ }
39
+ return '';
40
+ }
41
+
42
+ function preferredNvmBin(home, fileSystem = fs) {
43
+ const versionsRoot = path.join(home, '.nvm', 'versions', 'node');
44
+ let versions = [];
45
+ try {
46
+ versions = fileSystem.readdirSync(versionsRoot, { withFileTypes: true })
47
+ .filter(entry => entry.isDirectory() && versionParts(entry.name))
48
+ .map(entry => entry.name)
49
+ .sort(compareVersionNames);
50
+ } catch (_missingNvm) {
51
+ return '';
52
+ }
53
+ const selector = resolveNvmDefault(home, fileSystem);
54
+ const selectorParts = selector ? selector.split('.') : [];
55
+ const selected = versions.find(version => {
56
+ if (!selectorParts.length) return true;
57
+ const parts = String(version).replace(/^v/, '').split('.');
58
+ return selectorParts.every((part, index) => parts[index] === part);
59
+ });
60
+ return selected ? path.join(versionsRoot, selected, 'bin') : '';
61
+ }
62
+
63
+ function macPathEntries(home, pathValue = '', fileSystem = fs) {
64
+ const existing = String(pathValue || '').split(path.delimiter).filter(Boolean);
65
+ return unique([
66
+ ...existing,
67
+ preferredNvmBin(home, fileSystem),
68
+ path.join(home, '.volta', 'bin'),
69
+ path.join(home, '.local', 'share', 'mise', 'shims'),
70
+ path.join(home, '.asdf', 'shims'),
71
+ path.join(home, '.local', 'bin'),
72
+ path.join(home, '.cargo', 'bin'),
73
+ '/opt/homebrew/bin',
74
+ '/opt/homebrew/sbin',
75
+ '/usr/local/bin',
76
+ '/usr/local/sbin',
77
+ ]);
78
+ }
79
+
80
+ module.exports = { macPathEntries, preferredNvmBin, resolveNvmDefault };
@@ -61,7 +61,9 @@ function providerFromWindowsProcess(processInfo = {}) {
61
61
  const args = commandLine.toLowerCase().replace(/\\/g, '/');
62
62
  if (name === 'claude.exe') {
63
63
  if (args.includes('/windowsapps/claude_') || args.includes('--type=')) return null;
64
- if (/^(?:"?[a-z]:)?[^\r\n]*\/\.local\/bin\/claude\.exe"?\s*$/i.test(args) || /^claude(?:\.exe)?\s*$/i.test(args)) return 'claude';
64
+ if (/\bdaemon\s+run\b/i.test(args)) return null;
65
+ if (/^(?:"?[a-z]:)?[^\r\n]*\/\.local\/bin\/claude\.exe"?(?:\s|$)/i.test(args)
66
+ || /^claude(?:\.exe)?(?:\s|$)/i.test(args)) return 'claude';
65
67
  return null;
66
68
  }
67
69
  if (name === 'codex.exe') {
@@ -81,7 +83,7 @@ function providerFromWindowsProcess(processInfo = {}) {
81
83
  function providerFromPosixProcess(processInfo = {}) {
82
84
  const name = String(processInfo.name || processInfo.command || '').toLowerCase().split('/').pop();
83
85
  const args = String(processInfo.commandLine || processInfo.args || '').toLowerCase();
84
- if (name === 'claude') return /--type=|\/applications\/claude\.app/.test(args) ? null : 'claude';
86
+ if (name === 'claude') return /--type=|\/applications\/claude\.app|\bdaemon\s+run\b/.test(args) ? null : 'claude';
85
87
  if (name === 'codex' || /^codex-/.test(name)) return /\bapp-server\b|\/applications\/(?:chatgpt|codex)\.app/.test(args) ? null : 'codex';
86
88
  if (name === 'gemini') return 'gemini';
87
89
  if (name === 'grok') return 'grok';
@@ -129,10 +131,39 @@ function processRows(value) {
129
131
  })).filter(item => item.pid > 0);
130
132
  }
131
133
 
134
+ function commandArgument(commandLine, name) {
135
+ const escaped = String(name || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
136
+ const match = String(commandLine || '').match(new RegExp(`(?:^|\\s)${escaped}\\s+(?:"([^"]+)"|'([^']+)'|(\\S+))`, 'i'));
137
+ return match ? String(match[1] || match[2] || match[3] || '').trim() : '';
138
+ }
139
+
140
+ function processSessionExternalId(processInfo = {}, provider = '') {
141
+ const commandLine = String(processInfo.commandLine || processInfo.CommandLine || '');
142
+ if (provider === 'claude') {
143
+ const sessionId = commandArgument(commandLine, '--session-id');
144
+ if (sessionId) return pathlessSessionId(sessionId);
145
+ return pathlessSessionId(commandArgument(commandLine, '--resume'));
146
+ }
147
+ if (provider === 'codex') {
148
+ const match = commandLine.match(/(?:^|\s)resume\s+(?:--last\s+)?(?:"([^"]+)"|'([^']+)'|(\S+))/i);
149
+ return pathlessSessionId(match && (match[1] || match[2] || match[3]));
150
+ }
151
+ return '';
152
+ }
153
+
154
+ function pathlessSessionId(value) {
155
+ const text = String(value || '').trim().replace(/^"|"$/g, '');
156
+ if (!text) return '';
157
+ const basename = text.replace(/\\/g, '/').split('/').pop() || text;
158
+ return basename.replace(/\.jsonl$/i, '');
159
+ }
160
+
132
161
  function selectAgentProcesses(rows, options = {}) {
133
162
  const providerResolver = options.providerResolver || providerFromWindowsProcess;
134
163
  const environment = options.environment || 'windows';
135
- const candidates = rows.map(item => ({ ...item, provider: providerResolver(item) })).filter(item => item.provider);
164
+ const candidates = rows
165
+ .map(item => ({ ...item, provider: providerResolver(item) }))
166
+ .filter(item => item.provider && !utilityProcess(item));
136
167
  const byParent = new Map();
137
168
  for (const item of candidates) {
138
169
  if (!byParent.has(item.parentPid)) byParent.set(item.parentPid, []);
@@ -160,30 +191,38 @@ function selectAgentProcesses(rows, options = {}) {
160
191
  parentPid: item.parentPid,
161
192
  command: String(item.name || '').replace(/\.exe$/i, '').split(/[\\/]/).pop(),
162
193
  startedAt: item.startedAt,
194
+ externalId: processSessionExternalId(item, item.provider),
163
195
  })).sort((a, b) => a.provider.localeCompare(b.provider) || a.pid - b.pid);
164
196
  }
165
197
 
198
+ function utilityProcess(processInfo = {}) {
199
+ 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);
203
+ }
204
+
166
205
  function utilitySession(session) {
167
- return /^(?:extract durable memory candidates|approved command prefix saved|you are a memory extraction)/i.test(String(session.title || '').trim());
206
+ return Boolean(session && session.utilityKind)
207
+ || /^(?:extract durable memory candidates|approved command prefix saved|you are a memory extraction|reply with exactly ok\. do not use tools)/i.test(String(session && session.title || '').trim());
168
208
  }
169
209
 
170
210
  function runtimeLinkScore(session, processInfo, now = Date.now()) {
171
211
  if (!session || session.provider !== processInfo.provider) return -Infinity;
172
212
  if (!session.environment || session.environment.kind !== processInfo.environment) return -Infinity;
213
+ if (utilitySession(session)) return -Infinity;
214
+ if (/^(?:claude-desktop|codex-desktop|codex-ide)$/i.test(String(session.clientKind || ''))) return -Infinity;
173
215
  let score = session.parentId ? -800 : 2_000;
174
- if (utilitySession(session)) score -= 10_000;
175
216
  if (session.status === 'running' || session.status === 'starting') score += 3_000;
176
217
  else if (session.status === 'waiting') score += 1_000;
177
218
  const ageMinutes = Math.max(0, (now - Date.parse(session.updatedAt || 0)) / 60_000);
178
219
  score += Math.max(0, 2_880 - ageMinutes);
179
220
  const sessionStart = Date.parse(session.startedAt || 0);
180
221
  const processStart = Date.parse(processInfo.startedAt || 0);
181
- if (Number.isFinite(sessionStart) && Number.isFinite(processStart)) {
182
- const deltaMinutes = Math.abs(sessionStart - processStart) / 60_000;
183
- if (deltaMinutes <= 3) score += 6_000;
184
- else if (deltaMinutes <= 30) score += 2_500;
185
- else if (deltaMinutes <= 180) score += 800;
186
- }
222
+ if (!Number.isFinite(sessionStart) || !Number.isFinite(processStart)) return -Infinity;
223
+ const deltaMinutes = Math.abs(sessionStart - processStart) / 60_000;
224
+ if (deltaMinutes > 5) return -Infinity;
225
+ score += 6_000 - deltaMinutes * 200;
187
226
  return score;
188
227
  }
189
228
 
@@ -191,13 +230,6 @@ function markRuntime(session, presence) {
191
230
  const existing = Array.isArray(session.runtimePresence) ? session.runtimePresence : [];
192
231
  if (!existing.some(item => item.id === presence.id)) existing.push(presence);
193
232
  session.runtimePresence = existing;
194
- session.status = 'running';
195
- session.statusObserved = true;
196
- session.statusDetail = presence.kind === 'tmux'
197
- ? `tmux에서 AI 프로세스 실행 중 · PID ${presence.pid}`
198
- : (presence.kind === 'bridge'
199
- ? `안전하게 연결된 외부 터미널 · PID ${presence.pid}`
200
- : `AI CLI 프로세스 실행 중 · PID ${presence.pid}`);
201
233
  return session;
202
234
  }
203
235
 
@@ -243,6 +275,7 @@ function syntheticRuntimeSession(processInfo, now = Date.now()) {
243
275
  function bridgeLinkScore(session, bridge, now = Date.now()) {
244
276
  if (!session || session.provider !== bridge.provider) return -Infinity;
245
277
  if (!session.environment || session.environment.kind !== bridge.environment) return -Infinity;
278
+ if (utilitySession(session)) return -Infinity;
246
279
  if (session.clientKind === 'codex-desktop' || session.clientKind === 'codex-ide' || session.clientKind === 'claude-desktop') return -Infinity;
247
280
  let score = session.parentId ? -500 : 3_000;
248
281
  const sessionCwd = String(session.cwd || '').replace(/\\/g, '/').replace(/\/$/, '').toLowerCase();
@@ -280,7 +313,19 @@ function applyRuntimePresence(agentSessions, tmuxSnapshot, processSnapshot, now
280
313
  const usedBridgeIds = new Set();
281
314
  const bridgePairs = [];
282
315
  for (const bridge of bridges || []) {
316
+ const explicitId = [bridge.linkedSessionId, bridge.sessionId, bridge.bridgeId, bridge.id]
317
+ .map(value => String(value || ''))
318
+ .find(id => id && byId.has(id));
319
+ const linked = explicitId && byId.get(explicitId);
320
+ if (!linked || linked.provider !== bridge.provider || utilitySession(linked)) continue;
321
+ usedBridgeIds.add(bridge.id);
322
+ usedSessionIds.add(linked.id);
323
+ markRuntime(linked, { ...bridge, kind: 'bridge', label: 'LoadToAgent 세션 터미널', linkScore: 'explicit' });
324
+ }
325
+ for (const bridge of bridges || []) {
326
+ if (usedBridgeIds.has(bridge.id)) continue;
283
327
  for (const session of sessions) {
328
+ if (usedSessionIds.has(session.id)) continue;
284
329
  const score = bridgeLinkScore(session, bridge, now);
285
330
  if (score > 0) bridgePairs.push({ bridge, session, score });
286
331
  }
@@ -298,7 +343,7 @@ function applyRuntimePresence(agentSessions, tmuxSnapshot, processSnapshot, now
298
343
  for (const pane of window.panes || []) {
299
344
  const agent = pane.agent;
300
345
  const linked = agent && agent.linkedSessionId && byId.get(agent.linkedSessionId);
301
- if (!linked) continue;
346
+ if (!linked || pane.dead) continue;
302
347
  usedSessionIds.add(linked.id);
303
348
  markRuntime(linked, {
304
349
  id: `tmux:${distro.name}:${pane.nativeId}`,
@@ -316,8 +361,23 @@ function applyRuntimePresence(agentSessions, tmuxSnapshot, processSnapshot, now
316
361
 
317
362
  const bridgePids = new Set((bridges || []).map(item => Number(item.pid || 0)).filter(Boolean));
318
363
  const processes = (processSnapshot && processSnapshot.processes || []).filter(item => !bridgePids.has(Number(item.pid || 0)));
364
+ const usedProcessIds = new Set();
365
+ for (const processInfo of processes) {
366
+ const externalId = String(processInfo.externalId || '');
367
+ if (!externalId) continue;
368
+ const linked = sessions.find(session => !usedSessionIds.has(session.id)
369
+ && session.provider === processInfo.provider
370
+ && session.environment && session.environment.kind === processInfo.environment
371
+ && (String(session.externalId || '') === externalId || String(session.id || '').endsWith(`:${externalId}`)));
372
+ if (!linked) continue;
373
+ usedProcessIds.add(processInfo.id);
374
+ usedSessionIds.add(linked.id);
375
+ const label = processInfo.environment === 'macos' ? 'macOS CLI' : (processInfo.environment === 'linux' ? 'Linux CLI' : 'Windows CLI');
376
+ markRuntime(linked, { ...processInfo, kind: processInfo.environment || 'windows', label, linkScore: 'explicit-session-id' });
377
+ }
319
378
  const pairs = [];
320
379
  for (const processInfo of processes) {
380
+ if (usedProcessIds.has(processInfo.id)) continue;
321
381
  for (const session of sessions) {
322
382
  if (usedSessionIds.has(session.id)) continue;
323
383
  const score = runtimeLinkScore(session, processInfo, now);
@@ -325,7 +385,6 @@ function applyRuntimePresence(agentSessions, tmuxSnapshot, processSnapshot, now
325
385
  }
326
386
  }
327
387
  pairs.sort((a, b) => b.score - a.score);
328
- const usedProcessIds = new Set();
329
388
  for (const pair of pairs) {
330
389
  if (usedProcessIds.has(pair.processInfo.id) || usedSessionIds.has(pair.session.id)) continue;
331
390
  usedProcessIds.add(pair.processInfo.id);
@@ -333,9 +392,6 @@ function applyRuntimePresence(agentSessions, tmuxSnapshot, processSnapshot, now
333
392
  const label = pair.processInfo.environment === 'macos' ? 'macOS CLI' : (pair.processInfo.environment === 'linux' ? 'Linux CLI' : 'Windows CLI');
334
393
  markRuntime(pair.session, { ...pair.processInfo, kind: pair.processInfo.environment || 'windows', label, linkScore: Math.round(pair.score) });
335
394
  }
336
- for (const processInfo of processes) {
337
- if (!usedProcessIds.has(processInfo.id)) sessions.push(syntheticRuntimeSession(processInfo, now));
338
- }
339
395
  for (const bridge of bridges || []) {
340
396
  if (!usedBridgeIds.has(bridge.id)) sessions.push(syntheticBridgeSession(bridge, now));
341
397
  }
@@ -388,6 +444,8 @@ module.exports = {
388
444
  posixProcessRows,
389
445
  elapsedSeconds,
390
446
  selectAgentProcesses,
447
+ processSessionExternalId,
448
+ utilityProcess,
391
449
  runtimeLinkScore,
392
450
  bridgeLinkScore,
393
451
  applyRuntimePresence,
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ class ProviderVisibilityStore {
7
+ constructor(file, providerIds, onError = () => {}) {
8
+ this.file = file;
9
+ this.providerIds = new Set(providerIds);
10
+ this.onError = onError;
11
+ this.hidden = new Set();
12
+ }
13
+
14
+ normalize(value) {
15
+ const hidden = value && Array.isArray(value.hidden) ? value.hidden : [];
16
+ return new Set(hidden.filter(id => this.providerIds.has(id)));
17
+ }
18
+
19
+ load() {
20
+ try {
21
+ this.hidden = this.normalize(JSON.parse(fs.readFileSync(this.file, 'utf8')));
22
+ } catch (error) {
23
+ if (error && error.code !== 'ENOENT') this.onError(error);
24
+ this.hidden = new Set();
25
+ }
26
+ return this.snapshot();
27
+ }
28
+
29
+ save(value) {
30
+ this.hidden = this.normalize(value);
31
+ fs.mkdirSync(path.dirname(this.file), { recursive: true });
32
+ fs.writeFileSync(this.file, JSON.stringify(this.snapshot(), null, 2), 'utf8');
33
+ return this.snapshot();
34
+ }
35
+
36
+ isVisible(providerId) {
37
+ return !this.hidden.has(String(providerId || ''));
38
+ }
39
+
40
+ snapshot() {
41
+ return { hidden: [...this.hidden] };
42
+ }
43
+ }
44
+
45
+ module.exports = { ProviderVisibilityStore };