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.
- package/README.ko.md +7 -0
- package/README.md +7 -0
- package/README.zh-CN.md +7 -0
- package/docs/PROVIDER-CONTRACTS.md +23 -1
- package/docs/UI-AUDIT-100.md +118 -0
- package/docs/UI-AUDIT-101-200.md +120 -0
- package/docs/UI-AUDIT-201-300.md +120 -0
- package/main.js +183 -37
- package/package.json +15 -4
- package/preload.js +11 -0
- package/renderer/app-agent-actions.js +125 -53
- package/renderer/app-bootstrap.js +54 -9
- package/renderer/app-dashboard.js +197 -58
- package/renderer/app-drawer-content.js +103 -86
- package/renderer/app-drawer-data.js +26 -13
- package/renderer/app-drawer.js +79 -55
- package/renderer/app-events-dialogs.js +146 -37
- package/renderer/app-events-filters.js +172 -9
- package/renderer/app-events-navigation.js +78 -31
- package/renderer/app-events-sessions.js +171 -4
- package/renderer/app-events.js +2 -1
- package/renderer/app-graph-model.js +32 -4
- package/renderer/app-graph-orchestration.js +16 -13
- package/renderer/app-graph-view.js +228 -112
- package/renderer/app-management.js +211 -0
- package/renderer/app-provider-visibility.js +124 -0
- package/renderer/app-quality.js +568 -0
- package/renderer/app-run-modal.js +103 -33
- package/renderer/app-runtime-overview.js +312 -0
- package/renderer/app-session-render.js +94 -37
- package/renderer/app-tmux-render.js +119 -38
- package/renderer/app.js +155 -76
- package/renderer/i18n-messages.js +833 -15
- package/renderer/i18n.js +104 -0
- package/renderer/index.html +162 -75
- package/renderer/shared.js +17 -0
- package/renderer/styles-agent-map.css +50 -1
- package/renderer/styles-cards.css +26 -0
- package/renderer/styles-collaboration.css +18 -88
- package/renderer/styles-components.css +126 -14
- package/renderer/styles-management.css +355 -0
- package/renderer/styles-overlays.css +13 -2
- package/renderer/styles-product.css +24 -16
- package/renderer/styles-quality.css +312 -0
- package/renderer/styles-readability.css +862 -0
- package/renderer/styles-responsive-product.css +84 -12
- package/renderer/styles-responsive-runtime.css +31 -14
- package/renderer/styles-responsive-shell.css +44 -48
- package/renderer/styles-responsive-workflows.css +37 -17
- package/renderer/styles-run-composer.css +5 -0
- package/renderer/styles-runtime-overview.css +285 -0
- package/renderer/styles-settings.css +160 -0
- package/renderer/styles-terminal.css +339 -2
- package/renderer/styles-tmux.css +154 -0
- package/renderer/styles-workflow-map.css +362 -15
- package/renderer/styles-workflows.css +69 -2
- package/renderer/styles.css +27 -12
- package/renderer/terminal-agent.js +17 -13
- package/renderer/terminal-events.js +233 -40
- package/renderer/terminal-workbench.js +176 -78
- package/renderer/terminal.js +350 -37
- package/src/agentMonitor/claudeParser.js +96 -7
- package/src/agentMonitor/codexParser.js +59 -4
- package/src/agentMonitor/executionActivity.js +282 -0
- package/src/agentMonitor/genericParser.js +56 -13
- package/src/agentMonitor/hierarchy.js +17 -0
- package/src/agentMonitor/responseIntent.js +51 -0
- package/src/agentMonitor.js +21 -3
- package/src/agentRunner.js +66 -1
- package/src/attentionNotifier.js +71 -0
- package/src/automationMonitor.js +258 -0
- package/src/contracts.js +10 -1
- package/src/ipc/registerAgentIpc.js +9 -3
- package/src/ipc/registerAppIpc.js +4 -1
- package/src/ipc/registerTerminalIpc.js +12 -6
- package/src/macUpdateHelper.js +210 -0
- package/src/monitorWorker.js +65 -6
- package/src/platformPath.js +80 -0
- package/src/processMonitor.js +81 -23
- package/src/providerVisibilityStore.js +45 -0
- package/src/sessionIntelligence.js +247 -0
- package/src/terminalHost.js +381 -0
- package/src/terminalHostDaemon.js +60 -0
- package/src/terminalManager.js +158 -3
- package/src/tmuxMonitor.js +2 -2
- package/src/updateInstaller.js +175 -0
package/src/monitorWorker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 };
|
package/src/processMonitor.js
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
|
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)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 };
|