loadtoagent 1.3.2 → 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.
- package/main.js +53 -8
- package/package.json +2 -1
- package/renderer/app-agent-actions.js +172 -66
- package/renderer/app-bootstrap.js +3 -1
- package/renderer/app-dashboard.js +36 -7
- package/renderer/app-drawer-content.js +177 -59
- package/renderer/app-drawer-data.js +7 -3
- package/renderer/app-drawer.js +72 -33
- package/renderer/app-events-dialogs.js +8 -1
- package/renderer/app-events-navigation.js +7 -0
- package/renderer/app-events-sessions.js +136 -13
- package/renderer/app-graph-model.js +2 -5
- package/renderer/app-graph-orchestration.js +7 -18
- package/renderer/app-graph-view.js +194 -48
- package/renderer/app-management.js +330 -35
- package/renderer/app-quality.js +5 -0
- package/renderer/app-session-render.js +20 -83
- package/renderer/app.js +5 -1
- package/renderer/i18n-messages.js +232 -21
- package/renderer/index.html +9 -6
- package/renderer/styles-components.css +140 -0
- package/renderer/styles-control-room.css +1439 -0
- package/renderer/styles-management.css +390 -3
- package/renderer/styles-responsive-shell.css +5 -0
- package/renderer/styles-runtime-overview.css +20 -0
- package/renderer/terminal-agent.js +31 -7
- package/renderer/terminal.js +4 -1
- package/src/agentMonitor/claudeParser.js +8 -4
- package/src/agentMonitor/codexParser.js +5 -4
- package/src/agentMonitor/genericParser.js +7 -6
- package/src/agentMonitor.js +44 -4
- package/src/attentionNotifier.js +3 -0
- package/src/ipc/registerTerminalIpc.js +2 -2
- package/src/monitorWorker.js +1 -1
- package/src/processMonitor.js +68 -7
- package/src/terminalHost.js +6 -4
- package/src/terminalManager.js +16 -2
package/main.js
CHANGED
|
@@ -32,6 +32,8 @@ process.title = PRODUCT_NAME;
|
|
|
32
32
|
if (process.platform === 'win32') app.setAppUserModelId('com.wincube.loadtoagent');
|
|
33
33
|
|
|
34
34
|
const demoCapture = process.env.LOADTOAGENT_DEMO_CAPTURE === '1';
|
|
35
|
+
// 사용자 메시지와 실제 개입 요청을 안정적으로 구분할 때까지 알림 발송을 중지한다.
|
|
36
|
+
const ATTENTION_NOTIFICATIONS_ENABLED = false;
|
|
35
37
|
let mainWindow = null;
|
|
36
38
|
let monitorWorker = null;
|
|
37
39
|
let runner = null;
|
|
@@ -219,23 +221,35 @@ function createWindow() {
|
|
|
219
221
|
mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
|
|
220
222
|
const allowedUrl = pathToFileURL(path.join(__dirname, 'renderer', 'index.html')).href;
|
|
221
223
|
mainWindow.webContents.on('will-navigate', (event, url) => { if (url !== allowedUrl) event.preventDefault(); });
|
|
222
|
-
|
|
224
|
+
const showWindow = () => {
|
|
225
|
+
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
226
|
+
mainWindow.show();
|
|
227
|
+
mainWindow.focus();
|
|
228
|
+
};
|
|
229
|
+
const showFallback = setTimeout(showWindow, 2_000);
|
|
230
|
+
mainWindow.once('ready-to-show', () => {
|
|
231
|
+
clearTimeout(showFallback);
|
|
232
|
+
showWindow();
|
|
233
|
+
});
|
|
223
234
|
mainWindow.on('close', event => {
|
|
224
235
|
if (isQuitting || !backgroundTerminalSessions().length) return;
|
|
225
236
|
event.preventDefault();
|
|
226
237
|
mainWindow.hide();
|
|
227
238
|
ensureBackgroundTray();
|
|
228
239
|
});
|
|
229
|
-
mainWindow.on('closed', () => {
|
|
240
|
+
mainWindow.on('closed', () => {
|
|
241
|
+
clearTimeout(showFallback);
|
|
242
|
+
mainWindow = null;
|
|
243
|
+
});
|
|
230
244
|
}
|
|
231
245
|
|
|
232
246
|
function backgroundTerminalSessions() {
|
|
233
247
|
if (!terminalManager) return [];
|
|
234
|
-
return terminalManager.list().filter(session => session.status === 'running' || session.status === 'starting');
|
|
248
|
+
return terminalManager.list().filter(session => !session.transient && (session.status === 'running' || session.status === 'starting'));
|
|
235
249
|
}
|
|
236
250
|
|
|
237
251
|
function visibleTerminalSessions(sessions) {
|
|
238
|
-
return (sessions || []).filter(session => session.type !== 'agent' || isProviderVisible(session.provider));
|
|
252
|
+
return (sessions || []).filter(session => !session.transient && (session.type !== 'agent' || isProviderVisible(session.provider)));
|
|
239
253
|
}
|
|
240
254
|
|
|
241
255
|
function showMainWindow() {
|
|
@@ -334,6 +348,7 @@ function markRendererReady() {
|
|
|
334
348
|
|
|
335
349
|
function createAttentionNotifier() {
|
|
336
350
|
return new AttentionNotifier({
|
|
351
|
+
enabled: ATTENTION_NOTIFICATIONS_ENABLED,
|
|
337
352
|
Notification,
|
|
338
353
|
isSupported: () => Notification.isSupported(),
|
|
339
354
|
copy: session => {
|
|
@@ -364,6 +379,36 @@ function installationType() {
|
|
|
364
379
|
return fs.existsSync(path.join(__dirname, '.git')) ? 'source' : 'npm';
|
|
365
380
|
}
|
|
366
381
|
|
|
382
|
+
async function connectTerminalForStartup(timeoutMs = 4_000) {
|
|
383
|
+
const connection = terminalManager.connect();
|
|
384
|
+
let timedOut = false;
|
|
385
|
+
let timer = null;
|
|
386
|
+
connection.then(() => {
|
|
387
|
+
if (!timedOut) return;
|
|
388
|
+
const sessions = visibleTerminalSessions(terminalManager.list());
|
|
389
|
+
sendTerminal('terminals:state', { change: 'reconnected', session: null, sessions });
|
|
390
|
+
sendTerminal('terminals:connection', { state: 'connected', message: mainText('terminalHostReconnected') });
|
|
391
|
+
updateBackgroundTrayMenu();
|
|
392
|
+
}).catch(error => {
|
|
393
|
+
if (timedOut) reportRecoverableError('terminal-host-late-connect', error);
|
|
394
|
+
});
|
|
395
|
+
try {
|
|
396
|
+
await Promise.race([
|
|
397
|
+
connection,
|
|
398
|
+
new Promise((_, reject) => {
|
|
399
|
+
timer = setTimeout(() => {
|
|
400
|
+
timedOut = true;
|
|
401
|
+
reject(new Error('터미널 호스트 연결을 백그라운드에서 계속합니다.'));
|
|
402
|
+
}, timeoutMs);
|
|
403
|
+
}),
|
|
404
|
+
]);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
reportRecoverableError('terminal-host-startup-connect', error);
|
|
407
|
+
} finally {
|
|
408
|
+
if (timer) clearTimeout(timer);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
367
412
|
async function installDownloadedUpdate() {
|
|
368
413
|
if (!updateManager) throw new Error('업데이트 관리자가 준비되지 않았습니다.');
|
|
369
414
|
const downloaded = await updateManager.download();
|
|
@@ -428,7 +473,7 @@ async function setupRuntime() {
|
|
|
428
473
|
}
|
|
429
474
|
terminalManager.on('data', payload => sendTerminal('terminals:data', payload));
|
|
430
475
|
terminalManager.on('state', payload => {
|
|
431
|
-
if (!payload.session || payload.session.type !== 'agent' || isProviderVisible(payload.session.provider)) {
|
|
476
|
+
if (!payload.session || (!payload.session.transient && (payload.session.type !== 'agent' || isProviderVisible(payload.session.provider)))) {
|
|
432
477
|
sendTerminal('terminals:state', { ...payload, sessions: visibleTerminalSessions(payload.sessions) });
|
|
433
478
|
}
|
|
434
479
|
updateBackgroundTrayMenu();
|
|
@@ -450,7 +495,7 @@ async function setupRuntime() {
|
|
|
450
495
|
message: mainText('terminalHostReconnectFailed', { reason: error?.message || String(error) }),
|
|
451
496
|
});
|
|
452
497
|
});
|
|
453
|
-
await
|
|
498
|
+
await connectTerminalForStartup();
|
|
454
499
|
availability = probeProviders();
|
|
455
500
|
monitorWorker = new Worker(path.join(__dirname, 'src', 'monitorWorker.js'), {
|
|
456
501
|
workerData: { runsDir, home: os.homedir(), intervalMs: 1200, availability },
|
|
@@ -480,7 +525,7 @@ function bridgePresence() {
|
|
|
480
525
|
if (!terminalManager) return [];
|
|
481
526
|
const environment = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'macos' : 'linux');
|
|
482
527
|
return terminalManager.list()
|
|
483
|
-
.filter(session => session.type === 'agent' && (session.status === 'running' || session.status === 'starting'))
|
|
528
|
+
.filter(session => !session.transient && session.type === 'agent' && (session.status === 'running' || session.status === 'starting'))
|
|
484
529
|
.map(session => ({
|
|
485
530
|
id: session.bridgeId || session.id,
|
|
486
531
|
bridgeId: session.bridgeId || '',
|
|
@@ -528,7 +573,7 @@ function requestAgentDetail(sessionId) {
|
|
|
528
573
|
if (!pendingDetails.has(requestId)) return;
|
|
529
574
|
pendingDetails.delete(requestId);
|
|
530
575
|
resolve(null);
|
|
531
|
-
},
|
|
576
|
+
}, 15000);
|
|
532
577
|
pendingDetails.set(requestId, {
|
|
533
578
|
resolve: value => {
|
|
534
579
|
clearTimeout(timer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loadtoagent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "A local desktop dashboard for Claude, Codex, Gemini, and Grok sessions.",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"author": "wincube AX",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"test:scroll": "electron scripts/scroll-retention-check.js",
|
|
54
54
|
"audit:ui": "electron scripts/browser-audit-host.js",
|
|
55
55
|
"test:runtime-overview": "electron scripts/runtime-overview-visual.js",
|
|
56
|
+
"test:control-room": "electron scripts/control-room-visual.js",
|
|
56
57
|
"test:readability": "electron scripts/readability-visual-check.js",
|
|
57
58
|
"test:update:mac": "node scripts/mac-update-integration-test.js",
|
|
58
59
|
"demo:readme": "electron scripts/readme-demo.js",
|
|
@@ -45,27 +45,94 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function agentCommandRouteOptions(session) {
|
|
49
|
+
if (!session?.parentId) return [];
|
|
50
|
+
let parent = snapshotSession(session.parentId) || state.details.get(session.parentId) || null;
|
|
51
|
+
const visited = new Set([session.id]);
|
|
52
|
+
while (parent?.parentId && !visited.has(parent.id)) {
|
|
53
|
+
visited.add(parent.id);
|
|
54
|
+
parent = snapshotSession(parent.parentId) || state.details.get(parent.parentId) || parent;
|
|
55
|
+
}
|
|
56
|
+
const directTargets = isLiveSession(session) ? agentCommandTargets(session) : [];
|
|
57
|
+
const parentTargets = parent ? agentCommandTargets(parent) : [];
|
|
58
|
+
return [
|
|
59
|
+
{ id: "direct", label: t("agent.route_direct"), available: directTargets.length > 0, targetSession: session, targets: directTargets },
|
|
60
|
+
{ id: "parent", label: t("agent.route_parent"), available: Boolean(parent && (parentTargets.length || agentResumeSupport(parent).supported)), targetSession: parent, targets: parentTargets },
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function selectedAgentCommandRoute(session) {
|
|
65
|
+
if (!session?.parentId) return "direct";
|
|
66
|
+
const options = agentCommandRouteOptions(session);
|
|
67
|
+
const saved = state.agentCommandRoutes.get(session.id);
|
|
68
|
+
if (options.some(option => option.id === saved && option.available)) return saved;
|
|
69
|
+
const selected = options.find(option => option.id === "direct" && option.available)
|
|
70
|
+
|| options.find(option => option.id === "parent" && option.available)
|
|
71
|
+
|| options.find(option => option.id === "direct");
|
|
72
|
+
const route = selected?.id || "direct";
|
|
73
|
+
state.agentCommandRoutes.set(session.id, route);
|
|
74
|
+
return route;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function routedAgentCommandContext(session, requestedRoute = "") {
|
|
78
|
+
const route = requestedRoute || selectedAgentCommandRoute(session);
|
|
79
|
+
const options = agentCommandRouteOptions(session);
|
|
80
|
+
const selected = options.find(option => option.id === route) || null;
|
|
81
|
+
return {
|
|
82
|
+
route,
|
|
83
|
+
options,
|
|
84
|
+
targetSession: selected?.targetSession || session,
|
|
85
|
+
targets: selected?.targets || agentCommandTargets(session),
|
|
86
|
+
available: session?.parentId ? Boolean(selected?.available) : true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function agentCommandTargetKey(session, route = "direct") {
|
|
91
|
+
return session?.parentId ? `${session.id}:${route}` : session.id;
|
|
92
|
+
}
|
|
93
|
+
|
|
48
94
|
function agentControlMode(session, targets) {
|
|
49
|
-
if (targets.length) return "direct";
|
|
50
|
-
|
|
51
|
-
if (
|
|
95
|
+
if (targets.length && isLiveSession(session)) return "direct";
|
|
96
|
+
const resume = agentResumeSupport(session);
|
|
97
|
+
if (resume.supported) {
|
|
98
|
+
if (originAppInfo(session)) return "origin-resume";
|
|
99
|
+
return isLiveSession(session) ? "handoff" : "resume";
|
|
100
|
+
}
|
|
52
101
|
if (isLiveSession(session)) return "connect";
|
|
53
102
|
return "ended";
|
|
54
103
|
}
|
|
55
104
|
|
|
56
|
-
function agentCommandComposer(session) {
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
105
|
+
function agentCommandComposer(session, options = {}) {
|
|
106
|
+
const routingEnabled = Boolean(options.conversation && session.parentId);
|
|
107
|
+
const routeContext = routingEnabled
|
|
108
|
+
? routedAgentCommandContext(session)
|
|
109
|
+
: {
|
|
110
|
+
route: "direct",
|
|
111
|
+
options: [],
|
|
112
|
+
targetSession: session,
|
|
113
|
+
targets: agentCommandTargets(session),
|
|
114
|
+
available: true,
|
|
115
|
+
};
|
|
116
|
+
const { route, targetSession, targets, available: routeAvailable } = routeContext;
|
|
117
|
+
const mode = routingEnabled && !routeAvailable ? "ended" : agentControlMode(targetSession, targets);
|
|
118
|
+
const relayed = routingEnabled && route === "parent" && routeAvailable;
|
|
119
|
+
const targetKey = agentCommandTargetKey(session, route);
|
|
120
|
+
const savedTarget = state.agentCommandTargets.get(targetKey) || "";
|
|
121
|
+
const relayTarget = relayed ? targets.find((target) => target.kind === "terminal") || targets[0] || null : null;
|
|
122
|
+
const targetId = targets.some((target) => target.id === savedTarget)
|
|
123
|
+
? savedTarget
|
|
124
|
+
: targets.length === 1
|
|
125
|
+
? targets[0].id
|
|
126
|
+
: relayTarget?.id || "";
|
|
127
|
+
if (targetId) state.agentCommandTargets.set(targetKey, targetId);
|
|
62
128
|
const target = targets.find((item) => item.id === targetId) || null;
|
|
63
129
|
const draft = state.agentCommandDrafts.get(session.id) || "";
|
|
64
130
|
const sending = state.agentCommandSending.has(session.id);
|
|
65
131
|
const canSend = ((mode === "direct" && Boolean(target)) || ["resume", "handoff", "origin-resume"].includes(mode)) && !sending;
|
|
66
|
-
const origin = originAppInfo(
|
|
67
|
-
const status =
|
|
68
|
-
|
|
132
|
+
const origin = originAppInfo(targetSession);
|
|
133
|
+
const status = relayed
|
|
134
|
+
? t("agent.route_parent_status")
|
|
135
|
+
: mode === "direct"
|
|
69
136
|
? t("agent.direct_status", { target: targets.length === 1 ? target.label : t("agent.choose_terminal_count", { count: targets.length }) })
|
|
70
137
|
: mode === "handoff"
|
|
71
138
|
? t("agent.handoff_status")
|
|
@@ -75,11 +142,10 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
75
142
|
? t("agent.origin_resume_status")
|
|
76
143
|
: mode === "connect"
|
|
77
144
|
? t("agent.connect_status")
|
|
78
|
-
:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
mode === "direct"
|
|
145
|
+
: window.LoadToAgentI18n.t("ui.ended_session");
|
|
146
|
+
const help = relayed
|
|
147
|
+
? t("agent.route_parent_inline_help")
|
|
148
|
+
: mode === "direct"
|
|
83
149
|
? t("agent.direct_help")
|
|
84
150
|
: mode === "handoff"
|
|
85
151
|
? t("agent.handoff_help")
|
|
@@ -88,23 +154,31 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
88
154
|
: mode === "origin-resume"
|
|
89
155
|
? t("agent.origin_resume_help", { provider: (origin && origin.provider) || t("agent.desktop") })
|
|
90
156
|
: mode === "connect"
|
|
91
|
-
? t("agent.connect_help", { provider:
|
|
92
|
-
:
|
|
93
|
-
|
|
94
|
-
|
|
157
|
+
? t("agent.connect_help", { provider: targetSession.provider })
|
|
158
|
+
: agentResumeSupport(targetSession).reason || t("agent.resume_method_unknown");
|
|
159
|
+
const routePicker = routingEnabled
|
|
160
|
+
? `<div class="agent-command-route" role="group" aria-label="${esc(t("agent.route_label"))}">
|
|
161
|
+
<span>${esc(t("agent.route_label"))}</span>
|
|
162
|
+
<div>${routeContext.options.map(option => `<button type="button" data-agent-command-session="${esc(session.id)}" data-agent-command-route="${esc(option.id)}"
|
|
163
|
+
aria-pressed="${option.id === route ? "true" : "false"}" ${option.available ? "" : "disabled"}>${esc(option.label)}</button>`).join("")}</div>
|
|
164
|
+
<small class="${routeAvailable ? "available" : "unavailable"}">${esc(t(routeAvailable
|
|
165
|
+
? route === "direct" ? "agent.route_direct_available" : "agent.route_parent_available"
|
|
166
|
+
: route === "direct" ? "agent.route_direct_unavailable" : "agent.route_parent_unavailable"))}</small>
|
|
167
|
+
</div>`
|
|
168
|
+
: "";
|
|
95
169
|
const picker =
|
|
96
|
-
targets.length > 1
|
|
170
|
+
targets.length > 1 && !relayed
|
|
97
171
|
? `<label class="agent-command-target">
|
|
98
172
|
<span>${esc(t("agent.target_terminal"))}</span>
|
|
99
|
-
<select data-agent-command-target="${esc(
|
|
173
|
+
<select data-agent-command-target="${esc(targetKey)}">
|
|
100
174
|
<option value="">${esc(t("agent.choose_terminal"))}</option>
|
|
101
175
|
${targets.map((item) => `<option value="${esc(item.id)}" ${item.id === targetId ? "selected" : ""}>${esc(item.label)}</option>`).join("")}
|
|
102
176
|
</select>
|
|
103
177
|
</label>`
|
|
104
178
|
: "";
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
mode === "direct"
|
|
179
|
+
const actions = relayed
|
|
180
|
+
? `<button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.sending" : "agent.send_via_parent"))}</button>`
|
|
181
|
+
: mode === "direct"
|
|
108
182
|
? `<button type="button" data-agent-terminal-open="${esc(session.id)}" ${canSend ? "" : "disabled"}>${esc(t("agent.open_terminal"))}</button>
|
|
109
183
|
<button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.sending" : "agent.send_now"))}</button>`
|
|
110
184
|
: mode === "resume"
|
|
@@ -112,26 +186,25 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
112
186
|
: mode === "handoff"
|
|
113
187
|
? `<button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.handing_off" : "agent.handoff_and_send"))}</button>`
|
|
114
188
|
: mode === "origin-resume"
|
|
115
|
-
?
|
|
189
|
+
? `<button type="submit" ${canSend ? "" : "disabled"}>${esc(t(sending ? "agent.connecting" : "agent.background_and_send"))}</button>`
|
|
116
190
|
: mode === "connect"
|
|
117
|
-
|
|
118
|
-
:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const editable = ["direct", "resume", "handoff", "origin-resume"].includes(mode);
|
|
122
|
-
const placeholder = editable ? t("agent.command_example") : status;
|
|
191
|
+
? `<button type="button" data-agent-bridge-copy="${esc(targetSession.provider)}">${esc(t("agent.copy_bridge"))}</button>`
|
|
192
|
+
: "";
|
|
193
|
+
const editable = relayed || ["direct", "resume", "handoff", "origin-resume"].includes(mode);
|
|
194
|
+
const placeholder = editable ? t(options.conversation ? "agent.conversation_placeholder" : "agent.command_example") : status;
|
|
123
195
|
const availabilityClass = mode === "direct" ? "connected" : ["resume", "handoff", "origin-resume"].includes(mode) ? "resume-ready" : "unavailable";
|
|
124
|
-
return `<form class="agent-command-panel ${availabilityClass} control-${mode}"
|
|
125
|
-
data-agent-command-form="${esc(session.id)}">
|
|
196
|
+
return `<form class="agent-command-panel ${availabilityClass} control-${mode} ${options.conversation ? "conversation-composer" : ""}"
|
|
197
|
+
data-agent-command-form="${esc(session.id)}" data-agent-command-route-selected="${esc(route)}" data-agent-command-routing="${routingEnabled ? "conversation" : "session"}">
|
|
126
198
|
<header>
|
|
127
199
|
<span class="agent-command-icon" aria-hidden="true">›_</span>
|
|
128
|
-
<span><b>${esc(t("agent.command_title"))}</b><small>${esc(status)}</small></span>
|
|
200
|
+
<span><b>${esc(t(options.conversation ? "agent.conversation_title" : "agent.command_title"))}</b><small>${esc(status)}</small></span>
|
|
129
201
|
<i class="${mode === "direct" ? "connected" : ""}" aria-hidden="true"></i>
|
|
130
202
|
</header>
|
|
203
|
+
${routePicker}
|
|
131
204
|
${picker}
|
|
132
205
|
<label class="agent-command-input">
|
|
133
206
|
<span class="sr-only">${esc(t("agent.command_sr"))}</span>
|
|
134
|
-
<textarea data-agent-command-draft="${esc(session.id)}" maxlength="8000" rows="3"
|
|
207
|
+
<textarea data-agent-command-draft="${esc(session.id)}" maxlength="8000" rows="${options.conversation ? "2" : "3"}"
|
|
135
208
|
placeholder="${esc(placeholder)}" ${editable ? "" : "disabled"}>${editable ? esc(draft) : ""}</textarea>
|
|
136
209
|
</label>
|
|
137
210
|
<div class="agent-command-actions"><small aria-live="polite">${esc(help)}</small>${actions}</div>
|
|
@@ -148,9 +221,10 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
148
221
|
return ((state.snapshot && state.snapshot.sessions) || []).find((session) => session.id === id) || null;
|
|
149
222
|
}
|
|
150
223
|
|
|
151
|
-
function chosenAgentCommandTarget(session) {
|
|
152
|
-
const
|
|
153
|
-
const
|
|
224
|
+
function chosenAgentCommandTarget(session, requestedRoute = "") {
|
|
225
|
+
const routeContext = routedAgentCommandContext(session, requestedRoute);
|
|
226
|
+
const targets = routeContext.targets;
|
|
227
|
+
const saved = state.agentCommandTargets.get(agentCommandTargetKey(session, routeContext.route)) || "";
|
|
154
228
|
if (saved) return targets.find((target) => target.id === saved) || null;
|
|
155
229
|
return targets.length === 1 ? targets[0] : null;
|
|
156
230
|
}
|
|
@@ -181,16 +255,54 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
181
255
|
if (state.agentCommandSending.has(sessionId)) return;
|
|
182
256
|
const session = snapshotSession(sessionId);
|
|
183
257
|
if (!session || !window.LoadToAgentTerminal) return context.toast(t("agent.latest_not_found"));
|
|
184
|
-
const
|
|
258
|
+
const routingEnabled = form?.dataset.agentCommandRouting === "conversation" && Boolean(session.parentId);
|
|
259
|
+
const requestedRoute = routingEnabled ? form?.dataset.agentCommandRouteSelected || selectedAgentCommandRoute(session) : "direct";
|
|
260
|
+
const routeContext = routingEnabled
|
|
261
|
+
? routedAgentCommandContext(session, requestedRoute)
|
|
262
|
+
: { route: "direct", targetSession: session, targets: agentCommandTargets(session), available: true };
|
|
263
|
+
const targetSession = routeContext.targetSession;
|
|
264
|
+
const mode = routingEnabled && !routeContext.available ? "ended" : agentControlMode(targetSession, routeContext.targets);
|
|
265
|
+
if (routingEnabled && !["direct", "resume", "handoff", "origin-resume"].includes(mode)) return context.toast(t("agent.route_unavailable"));
|
|
266
|
+
const input = form.querySelector("[data-agent-command-draft]");
|
|
267
|
+
const command = String((input && input.value) || "").trim();
|
|
268
|
+
const routedCommand = routingEnabled && routeContext.route === "parent"
|
|
269
|
+
? t("agent.route_via_parent_prompt", {
|
|
270
|
+
task: session.delegation?.taskName || session.taskName || session.agentName || session.title,
|
|
271
|
+
message: command,
|
|
272
|
+
})
|
|
273
|
+
: command;
|
|
185
274
|
if (mode === "resume" || mode === "handoff" || mode === "origin-resume") {
|
|
186
|
-
const input = form.querySelector("[data-agent-command-draft]");
|
|
187
275
|
if (input) state.agentCommandDrafts.set(sessionId, input.value);
|
|
188
|
-
if (!
|
|
276
|
+
if (!command) return context.toast(t("agent.enter_command"));
|
|
277
|
+
if (routingEnabled && routeContext.route === "parent") {
|
|
278
|
+
state.agentCommandSending.add(sessionId);
|
|
279
|
+
const submit = form.querySelector('[type="submit"]');
|
|
280
|
+
if (submit) {
|
|
281
|
+
submit.disabled = true;
|
|
282
|
+
submit.textContent = t("agent.sending");
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
await window.LoadToAgentTerminal.resumeForAgent(targetSession, routedCommand, true, { focus: false });
|
|
286
|
+
state.agentCommandDrafts.delete(sessionId);
|
|
287
|
+
if (input) input.value = "";
|
|
288
|
+
context.toast(t("agent.command_routed_via_parent"));
|
|
289
|
+
} catch (error) {
|
|
290
|
+
context.toast(errorText(error, "agent.send_failed"));
|
|
291
|
+
} finally {
|
|
292
|
+
state.agentCommandSending.delete(sessionId);
|
|
293
|
+
if (submit && submit.isConnected) {
|
|
294
|
+
submit.disabled = false;
|
|
295
|
+
submit.textContent = t("agent.send_via_parent");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
189
300
|
return resumeAgentTerminal(sessionId, true);
|
|
190
301
|
}
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
302
|
+
const savedTarget = state.agentCommandTargets.get(agentCommandTargetKey(session, routeContext.route)) || "";
|
|
303
|
+
const target = savedTarget
|
|
304
|
+
? routeContext.targets.find((item) => item.id === savedTarget) || null
|
|
305
|
+
: routeContext.targets.length === 1 ? routeContext.targets[0] : null;
|
|
194
306
|
if (!target)
|
|
195
307
|
return context.toast(t(agentCommandTargets(session).length ? "agent.select_target_first" : "agent.no_writable_terminal"));
|
|
196
308
|
if (!command) return context.toast(t("agent.enter_command"));
|
|
@@ -201,11 +313,15 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
201
313
|
submit.textContent = t("agent.sending");
|
|
202
314
|
}
|
|
203
315
|
try {
|
|
204
|
-
await window.LoadToAgentTerminal.dispatchAgentCommand(
|
|
316
|
+
await window.LoadToAgentTerminal.dispatchAgentCommand(targetSession, routedCommand, target.id);
|
|
205
317
|
state.agentCommandDrafts.delete(sessionId);
|
|
206
318
|
if (input) input.value = "";
|
|
207
|
-
context.toast(t("agent.command_sent", { target: target.label }));
|
|
319
|
+
context.toast(t(routingEnabled && routeContext.route === "parent" ? "agent.command_routed_via_parent" : "agent.command_sent", { target: target.label }));
|
|
208
320
|
} catch (error) {
|
|
321
|
+
if (session.parentId) {
|
|
322
|
+
context.toast(errorText(error, "agent.send_failed"));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
209
325
|
const latest = snapshotSession(sessionId) || session;
|
|
210
326
|
const support = agentResumeSupport(latest);
|
|
211
327
|
if (!agentCommandTargets(latest).length && support.supported) {
|
|
@@ -224,7 +340,7 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
224
340
|
state.agentCommandSending.delete(sessionId);
|
|
225
341
|
if (submit && submit.isConnected) {
|
|
226
342
|
submit.disabled = false;
|
|
227
|
-
submit.textContent = t("agent.send_now");
|
|
343
|
+
submit.textContent = t(routingEnabled && routeContext.route === "parent" ? "agent.send_via_parent" : "agent.send_now");
|
|
228
344
|
}
|
|
229
345
|
}
|
|
230
346
|
}
|
|
@@ -232,12 +348,13 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
232
348
|
async function openAgentTerminal(sessionId) {
|
|
233
349
|
const session = snapshotSession(sessionId);
|
|
234
350
|
if (!session || !window.LoadToAgentTerminal) return context.toast(t("agent.terminal_info_not_found"));
|
|
235
|
-
const
|
|
351
|
+
const routeContext = routedAgentCommandContext(session);
|
|
352
|
+
const target = chosenAgentCommandTarget(session, routeContext.route);
|
|
236
353
|
if (!target)
|
|
237
|
-
return context.toast(t(
|
|
354
|
+
return context.toast(t(routeContext.targets.length ? "agent.select_open_target" : "agent.no_writable_terminal"));
|
|
238
355
|
selectView(target.kind === "tmux" ? "tmux" : "terminal");
|
|
239
356
|
try {
|
|
240
|
-
await window.LoadToAgentTerminal.openForAgent(
|
|
357
|
+
await window.LoadToAgentTerminal.openForAgent(routeContext.targetSession, target.id, state.agentCommandDrafts.get(sessionId) || "");
|
|
241
358
|
document.querySelector(".main-stage")?.scrollTo({ top: 0, behavior: "auto" });
|
|
242
359
|
} catch (error) {
|
|
243
360
|
context.toast(errorText(error, "agent.open_terminal_failed"));
|
|
@@ -256,19 +373,6 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
256
373
|
}
|
|
257
374
|
}
|
|
258
375
|
|
|
259
|
-
async function openSessionOrigin(sessionId) {
|
|
260
|
-
const session = snapshotSession(sessionId);
|
|
261
|
-
const origin = originAppInfo(session);
|
|
262
|
-
if (!session || !origin) return context.toast(t("agent.origin_not_found"));
|
|
263
|
-
try {
|
|
264
|
-
const result = await window.loadtoagent.openSessionOrigin(session);
|
|
265
|
-
if (!result || !result.ok) return context.toast(t("agent.origin_cannot_open", { app: origin.label }));
|
|
266
|
-
context.toast(t("agent.origin_opened", { provider: origin.provider }));
|
|
267
|
-
} catch (error) {
|
|
268
|
-
context.toast(errorText(error, "agent.origin_open_failed", { app: origin.label }));
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
376
|
async function controlManagedRun(sessionId, action) {
|
|
273
377
|
const session = snapshotSession(sessionId) || state.details.get(sessionId);
|
|
274
378
|
const runId = session && session.runId;
|
|
@@ -341,6 +445,9 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
341
445
|
agentResumeSupport,
|
|
342
446
|
originAppInfo,
|
|
343
447
|
agentControlMode,
|
|
448
|
+
agentCommandRouteOptions,
|
|
449
|
+
selectedAgentCommandRoute,
|
|
450
|
+
routedAgentCommandContext,
|
|
344
451
|
agentCommandComposer,
|
|
345
452
|
selectedSession,
|
|
346
453
|
snapshotSession,
|
|
@@ -349,7 +456,6 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
349
456
|
dispatchAgentCommand,
|
|
350
457
|
openAgentTerminal,
|
|
351
458
|
copyBridgeCommand,
|
|
352
|
-
openSessionOrigin,
|
|
353
459
|
controlManagedRun,
|
|
354
460
|
quickRespond,
|
|
355
461
|
prepareReassignment,
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
].forEach(install);
|
|
36
36
|
window.LoadToAgentApp = app;
|
|
37
37
|
|
|
38
|
-
const { $, esc, state, loadGuideState, loadQualityState = () => {}, loadProviderVisibility, projectVisibleSnapshot, visibleSnapshot, isProviderVisible, bindEvents, render, timeOnly, loadSessionDetail, renderUpdateSettings, syncViewChrome, selectView, openDrawer, openSubagentConversation, toast } = app;
|
|
38
|
+
const { $, esc, state, loadGuideState, loadQualityState = () => {}, saveDashboardPreferences = () => {}, loadProviderVisibility, projectVisibleSnapshot, visibleSnapshot, isProviderVisible, bindEvents, render, timeOnly, loadSessionDetail, renderUpdateSettings, syncViewChrome, selectView, openDrawer, openSubagentConversation, toast } = app;
|
|
39
39
|
|
|
40
40
|
let initializationError = "";
|
|
41
41
|
const showInitializationError = (message) => {
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
if (window.loadtoagent.onAttentionRequested) window.loadtoagent.onAttentionRequested(handleAttentionRequested);
|
|
92
92
|
bindEvents();
|
|
93
93
|
render();
|
|
94
|
+
saveDashboardPreferences();
|
|
94
95
|
$("#appConnectionState")?.classList.remove("connection-error");
|
|
95
96
|
$("#appErrorBanner").classList.add("hidden");
|
|
96
97
|
app.initialized = true;
|
|
@@ -101,6 +102,7 @@
|
|
|
101
102
|
if (window.LoadToAgentTerminal) window.LoadToAgentTerminal.updateSnapshot(visibleSnapshot(), state.workspaces);
|
|
102
103
|
$("#lastSync").textContent = timeOnly(snapshot.generatedAt);
|
|
103
104
|
render();
|
|
105
|
+
saveDashboardPreferences();
|
|
104
106
|
if (state.selectedId && $("#detailDrawer").classList.contains("open") && !state.detailLoadingIds.has(state.selectedId)) {
|
|
105
107
|
const card = (snapshot.sessions || []).find((session) => session.id === state.selectedId);
|
|
106
108
|
const detail = state.details.get(state.selectedId);
|
|
@@ -441,15 +441,42 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
441
441
|
}
|
|
442
442
|
if (state.sort === "tokens") sessions.sort((a, b) => Number((b.usage && b.usage.total) || 0) - Number((a.usage && a.usage.total) || 0));
|
|
443
443
|
else if (state.sort === "context") sessions.sort((a, b) => Number((b.context && b.context.percent) || 0) - Number((a.context && a.context.percent) || 0));
|
|
444
|
-
else
|
|
445
|
-
sessions.sort((a, b) => {
|
|
446
|
-
const activeA = a.status === "running" || a.status === "starting" ? 1 : 0;
|
|
447
|
-
const activeB = b.status === "running" || b.status === "starting" ? 1 : 0;
|
|
448
|
-
return activeB - activeA || Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
|
|
449
|
-
});
|
|
444
|
+
else sessions = stableSessionSort(sessions);
|
|
450
445
|
return sessions;
|
|
451
446
|
}
|
|
452
447
|
|
|
448
|
+
function ensureSessionOrder(sessions = []) {
|
|
449
|
+
if (!Array.isArray(state.sessionOrder)) state.sessionOrder = [];
|
|
450
|
+
const known = new Set(state.sessionOrder);
|
|
451
|
+
for (const session of sessions) {
|
|
452
|
+
const id = String(session?.id || "");
|
|
453
|
+
if (!id || known.has(id)) continue;
|
|
454
|
+
state.sessionOrder.push(id);
|
|
455
|
+
known.add(id);
|
|
456
|
+
}
|
|
457
|
+
return state.sessionOrder;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function stableSessionSort(sessions = []) {
|
|
461
|
+
const order = ensureSessionOrder(sessions);
|
|
462
|
+
const rank = new Map(order.map((id, index) => [id, index]));
|
|
463
|
+
return [...sessions].sort((a, b) => (rank.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b.id) ?? Number.MAX_SAFE_INTEGER));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function moveSessionOrder(sourceId, targetId, placeAfter = false) {
|
|
467
|
+
const source = String(sourceId || "");
|
|
468
|
+
const target = String(targetId || "");
|
|
469
|
+
if (!source || !target || source === target) return false;
|
|
470
|
+
const order = ensureSessionOrder(displaySessions());
|
|
471
|
+
const sourceIndex = order.indexOf(source);
|
|
472
|
+
if (sourceIndex < 0 || !order.includes(target)) return false;
|
|
473
|
+
order.splice(sourceIndex, 1);
|
|
474
|
+
const targetIndex = order.indexOf(target);
|
|
475
|
+
order.splice(targetIndex + (placeAfter ? 1 : 0), 0, source);
|
|
476
|
+
state.sessionOrder = order;
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
|
|
453
480
|
function graphFilteredSessions() {
|
|
454
481
|
let sessions = displaySessions();
|
|
455
482
|
if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
|
|
@@ -462,7 +489,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
462
489
|
.toLowerCase()
|
|
463
490
|
.includes(query),
|
|
464
491
|
);
|
|
465
|
-
return sessions;
|
|
492
|
+
return stableSessionSort(sessions);
|
|
466
493
|
}
|
|
467
494
|
|
|
468
495
|
function renderProviderVisibilitySettings() {
|
|
@@ -501,6 +528,8 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
501
528
|
announceProviderFilter,
|
|
502
529
|
filteredSessions,
|
|
503
530
|
graphFilteredSessions,
|
|
531
|
+
stableSessionSort,
|
|
532
|
+
moveSessionOrder,
|
|
504
533
|
renderProviderVisibilitySettings,
|
|
505
534
|
};
|
|
506
535
|
};
|