loadtoagent 1.3.7 → 1.3.8
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 +3 -2
- package/package.json +2 -1
- package/renderer/app-agent-actions.js +57 -22
- package/renderer/app-dashboard.js +56 -13
- package/renderer/app-drawer-content.js +78 -8
- package/renderer/app-events-filters.js +19 -23
- package/renderer/app-events-sessions.js +98 -0
- package/renderer/app-graph-model.js +6 -2
- package/renderer/app-graph-orchestration.js +12 -18
- package/renderer/app-graph-view.js +17 -6
- package/renderer/app-quality.js +5 -0
- package/renderer/app.js +13 -5
- package/renderer/i18n-messages.js +15 -0
- package/renderer/index.html +4 -6
- package/renderer/styles-components.css +77 -0
- package/renderer/styles-control-room.css +64 -43
- package/renderer/terminal-agent.js +14 -0
- package/src/agentMonitor/claudeParser.js +85 -10
- package/src/agentMonitor/hierarchy.js +7 -2
- package/src/agentMonitor.js +70 -1
- package/src/processMonitor.js +5 -0
- package/src/terminalHost.js +1 -1
- package/src/terminalManager.js +15 -3
package/main.js
CHANGED
|
@@ -523,7 +523,7 @@ async function setupRuntime() {
|
|
|
523
523
|
|
|
524
524
|
function bridgePresence() {
|
|
525
525
|
if (!terminalManager) return [];
|
|
526
|
-
const
|
|
526
|
+
const localEnvironment = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'macos' : 'linux');
|
|
527
527
|
return terminalManager.list()
|
|
528
528
|
.filter(session => !session.transient && session.type === 'agent' && (session.status === 'running' || session.status === 'starting'))
|
|
529
529
|
.map(session => ({
|
|
@@ -535,7 +535,8 @@ function bridgePresence() {
|
|
|
535
535
|
pid: session.pid,
|
|
536
536
|
cwd: session.cwd,
|
|
537
537
|
startedAt: session.createdAt,
|
|
538
|
-
environment,
|
|
538
|
+
environment: session.distro && process.platform === 'win32' ? 'wsl' : localEnvironment,
|
|
539
|
+
distro: session.distro || '',
|
|
539
540
|
kind: 'bridge',
|
|
540
541
|
label: 'LoadToAgent 외부 터미널 브리지',
|
|
541
542
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loadtoagent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.8",
|
|
4
4
|
"description": "A local desktop dashboard for Claude, Codex, Gemini, and Grok sessions.",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"author": "wincube AX",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"scripts": {
|
|
44
44
|
"start": "electron .",
|
|
45
45
|
"test": "node scripts/regression-test.js",
|
|
46
|
+
"test:accuracy": "node scripts/accuracy-evaluation.js",
|
|
46
47
|
"test:terminal": "electron scripts/terminal-integration-test.js",
|
|
47
48
|
"test:terminal:mac-stress": "node scripts/mac-terminal-stress-check.js",
|
|
48
49
|
"test:terminal-restart": "node scripts/terminal-restart-persistence-check.js",
|
|
@@ -91,6 +91,39 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
91
91
|
return session?.parentId ? `${session.id}:${route}` : session.id;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function conversationMessageKey(message) {
|
|
95
|
+
const id = String(message?.id || "").trim();
|
|
96
|
+
if (id) return `id:${id}`;
|
|
97
|
+
return `${message?.role || ""}:${String(message?.text || "").replace(/\s+/g, " ").trim()}:${message?.timestamp || ""}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function beginConversationMessage(session, command) {
|
|
101
|
+
const detail = state.details.get(session.id);
|
|
102
|
+
const baselineMessages = [...(session.messages || []), ...(detail?.messages || [])];
|
|
103
|
+
const entry = {
|
|
104
|
+
id: `local:${session.id}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,
|
|
105
|
+
text: command,
|
|
106
|
+
timestamp: new Date().toISOString(),
|
|
107
|
+
status: "sending",
|
|
108
|
+
presented: false,
|
|
109
|
+
baselineMessageKeys: new Set(baselineMessages.map(conversationMessageKey)),
|
|
110
|
+
};
|
|
111
|
+
const pending = state.pendingConversationMessages.get(session.id) || [];
|
|
112
|
+
pending.push(entry);
|
|
113
|
+
state.pendingConversationMessages.set(session.id, pending);
|
|
114
|
+
state.drawerForceLatest = true;
|
|
115
|
+
context.renderDrawer?.();
|
|
116
|
+
return entry;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function updateConversationMessage(sessionId, entry, status, error = "") {
|
|
120
|
+
if (!entry) return;
|
|
121
|
+
entry.status = status;
|
|
122
|
+
entry.error = error;
|
|
123
|
+
state.drawerForceLatest = true;
|
|
124
|
+
context.renderDrawer?.();
|
|
125
|
+
}
|
|
126
|
+
|
|
94
127
|
function agentControlMode(session, targets) {
|
|
95
128
|
if (targets.length && isLiveSession(session)) return "direct";
|
|
96
129
|
const resume = agentResumeSupport(session);
|
|
@@ -194,7 +227,7 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
194
227
|
const placeholder = editable ? t(options.conversation ? "agent.conversation_placeholder" : "agent.command_example") : status;
|
|
195
228
|
const availabilityClass = mode === "direct" ? "connected" : ["resume", "handoff", "origin-resume"].includes(mode) ? "resume-ready" : "unavailable";
|
|
196
229
|
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="${
|
|
230
|
+
data-agent-command-form="${esc(session.id)}" data-agent-command-route-selected="${esc(route)}" data-agent-command-routing="${options.conversation ? "conversation" : "session"}">
|
|
198
231
|
<header>
|
|
199
232
|
<span class="agent-command-icon" aria-hidden="true">›_</span>
|
|
200
233
|
<span><b>${esc(t(options.conversation ? "agent.conversation_title" : "agent.command_title"))}</b><small>${esc(status)}</small></span>
|
|
@@ -255,7 +288,8 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
255
288
|
if (state.agentCommandSending.has(sessionId)) return;
|
|
256
289
|
const session = snapshotSession(sessionId);
|
|
257
290
|
if (!session || !window.LoadToAgentTerminal) return context.toast(t("agent.latest_not_found"));
|
|
258
|
-
const
|
|
291
|
+
const conversationSubmission = form?.dataset.agentCommandRouting === "conversation";
|
|
292
|
+
const routingEnabled = conversationSubmission && Boolean(session.parentId);
|
|
259
293
|
const requestedRoute = routingEnabled ? form?.dataset.agentCommandRouteSelected || selectedAgentCommandRoute(session) : "direct";
|
|
260
294
|
const routeContext = routingEnabled
|
|
261
295
|
? routedAgentCommandContext(session, requestedRoute)
|
|
@@ -274,26 +308,20 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
274
308
|
if (mode === "resume" || mode === "handoff" || mode === "origin-resume") {
|
|
275
309
|
if (input) state.agentCommandDrafts.set(sessionId, input.value);
|
|
276
310
|
if (!command) return context.toast(t("agent.enter_command"));
|
|
277
|
-
if (
|
|
311
|
+
if (conversationSubmission) {
|
|
278
312
|
state.agentCommandSending.add(sessionId);
|
|
279
|
-
const
|
|
280
|
-
if (submit) {
|
|
281
|
-
submit.disabled = true;
|
|
282
|
-
submit.textContent = t("agent.sending");
|
|
283
|
-
}
|
|
313
|
+
const pendingMessage = beginConversationMessage(session, command);
|
|
284
314
|
try {
|
|
285
315
|
await window.LoadToAgentTerminal.resumeForAgent(targetSession, routedCommand, true, { focus: false });
|
|
286
316
|
state.agentCommandDrafts.delete(sessionId);
|
|
287
|
-
|
|
288
|
-
context.toast(t("agent.command_routed_via_parent"));
|
|
317
|
+
updateConversationMessage(sessionId, pendingMessage, "awaiting");
|
|
318
|
+
context.toast(t(routingEnabled && routeContext.route === "parent" ? "agent.command_routed_via_parent" : "agent.command_sent_background"));
|
|
289
319
|
} catch (error) {
|
|
320
|
+
updateConversationMessage(sessionId, pendingMessage, "failed", errorText(error, "agent.send_failed"));
|
|
290
321
|
context.toast(errorText(error, "agent.send_failed"));
|
|
291
322
|
} finally {
|
|
292
323
|
state.agentCommandSending.delete(sessionId);
|
|
293
|
-
|
|
294
|
-
submit.disabled = false;
|
|
295
|
-
submit.textContent = t("agent.send_via_parent");
|
|
296
|
-
}
|
|
324
|
+
context.renderDrawer?.();
|
|
297
325
|
}
|
|
298
326
|
return;
|
|
299
327
|
}
|
|
@@ -307,6 +335,8 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
307
335
|
return context.toast(t(agentCommandTargets(session).length ? "agent.select_target_first" : "agent.no_writable_terminal"));
|
|
308
336
|
if (!command) return context.toast(t("agent.enter_command"));
|
|
309
337
|
state.agentCommandSending.add(sessionId);
|
|
338
|
+
if (input) state.agentCommandDrafts.set(sessionId, input.value);
|
|
339
|
+
const pendingMessage = conversationSubmission ? beginConversationMessage(session, command) : null;
|
|
310
340
|
const submit = form.querySelector('[type="submit"]');
|
|
311
341
|
if (submit) {
|
|
312
342
|
submit.disabled = true;
|
|
@@ -316,28 +346,33 @@ window.LoadToAgentAppFactories.createAgentActions = function createAgentActions(
|
|
|
316
346
|
await window.LoadToAgentTerminal.dispatchAgentCommand(targetSession, routedCommand, target.id);
|
|
317
347
|
state.agentCommandDrafts.delete(sessionId);
|
|
318
348
|
if (input) input.value = "";
|
|
349
|
+
updateConversationMessage(sessionId, pendingMessage, "awaiting");
|
|
319
350
|
context.toast(t(routingEnabled && routeContext.route === "parent" ? "agent.command_routed_via_parent" : "agent.command_sent", { target: target.label }));
|
|
320
351
|
} catch (error) {
|
|
321
|
-
|
|
322
|
-
context.toast(errorText(error, "agent.send_failed"));
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
const latest = snapshotSession(sessionId) || session;
|
|
352
|
+
const latest = snapshotSession(targetSession.id) || targetSession;
|
|
326
353
|
const support = agentResumeSupport(latest);
|
|
327
|
-
|
|
354
|
+
const shouldRecover = support.supported
|
|
355
|
+
&& (conversationSubmission || !agentCommandTargets(latest).length);
|
|
356
|
+
if (shouldRecover) {
|
|
328
357
|
try {
|
|
329
358
|
state.agentCommandDrafts.set(sessionId, command);
|
|
330
|
-
await window.LoadToAgentTerminal.resumeForAgent(latest,
|
|
359
|
+
await window.LoadToAgentTerminal.resumeForAgent(latest, routedCommand, true, { focus: conversationSubmission ? false : true });
|
|
331
360
|
state.agentCommandDrafts.delete(sessionId);
|
|
332
361
|
if (input) input.value = "";
|
|
362
|
+
updateConversationMessage(sessionId, pendingMessage, "awaiting");
|
|
333
363
|
context.toast(t("agent.recovered_and_sent"));
|
|
334
364
|
return;
|
|
335
365
|
} catch (resumeError) {
|
|
366
|
+
updateConversationMessage(sessionId, pendingMessage, "failed", errorText(resumeError, "agent.recovery_failed"));
|
|
336
367
|
context.toast(errorText(resumeError, "agent.recovery_failed"));
|
|
337
368
|
}
|
|
338
|
-
} else
|
|
369
|
+
} else {
|
|
370
|
+
updateConversationMessage(sessionId, pendingMessage, "failed", errorText(error, "agent.send_failed"));
|
|
371
|
+
context.toast(errorText(error, "agent.send_failed"));
|
|
372
|
+
}
|
|
339
373
|
} finally {
|
|
340
374
|
state.agentCommandSending.delete(sessionId);
|
|
375
|
+
if (conversationSubmission) context.renderDrawer?.();
|
|
341
376
|
if (submit && submit.isConnected) {
|
|
342
377
|
submit.disabled = false;
|
|
343
378
|
submit.textContent = t(routingEnabled && routeContext.route === "parent" ? "agent.send_via_parent" : "agent.send_now");
|
|
@@ -51,7 +51,11 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function normalizedProjectPath(value) {
|
|
54
|
-
|
|
54
|
+
let normalized = String(value || "").trim().replace(/\\/g, "/");
|
|
55
|
+
normalized = normalized.replace(/^\/\/\?\/([a-z]:\/)/i, "$1");
|
|
56
|
+
const wslWindowsMount = normalized.match(/^\/mnt\/([a-z])(?:\/(.*))?$/i);
|
|
57
|
+
if (wslWindowsMount) normalized = `${wslWindowsMount[1]}:/${wslWindowsMount[2] || ""}`;
|
|
58
|
+
return normalized.replace(/\/+$/, "").toLocaleLowerCase();
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
function projectContainsPath(projectPath, candidatePath) {
|
|
@@ -67,8 +71,15 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
67
71
|
|
|
68
72
|
function observedProjects(sessions = displaySessions().filter((session) => !session.parentId)) {
|
|
69
73
|
const projects = new Map();
|
|
70
|
-
const saved = state.workspaces.map((item) => ({ ...item, key: normalizedProjectPath(item.path) }));
|
|
71
|
-
saved.forEach((item) => projects.set(item.key, {
|
|
74
|
+
const saved = state.workspaces.map((item, index) => ({ ...item, key: normalizedProjectPath(item.path), order: index }));
|
|
75
|
+
saved.forEach((item) => projects.set(item.key, {
|
|
76
|
+
path: item.path,
|
|
77
|
+
name: item.name || projectName(item.path),
|
|
78
|
+
saved: true,
|
|
79
|
+
order: item.order,
|
|
80
|
+
count: 0,
|
|
81
|
+
liveCount: 0,
|
|
82
|
+
}));
|
|
72
83
|
sessions.filter((session) => !session.parentId && !isProjectlessSession(session)).forEach((session) => {
|
|
73
84
|
const originPath = sessionOriginPath(session);
|
|
74
85
|
if (!originPath) return;
|
|
@@ -77,8 +88,16 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
77
88
|
.sort((a, b) => b.key.length - a.key.length)[0];
|
|
78
89
|
const path = owner ? owner.path : originPath;
|
|
79
90
|
const key = normalizedProjectPath(path);
|
|
80
|
-
const project = projects.get(key) || {
|
|
91
|
+
const project = projects.get(key) || {
|
|
92
|
+
path,
|
|
93
|
+
name: projectName(path),
|
|
94
|
+
saved: false,
|
|
95
|
+
order: Number.MAX_SAFE_INTEGER,
|
|
96
|
+
count: 0,
|
|
97
|
+
liveCount: 0,
|
|
98
|
+
};
|
|
81
99
|
project.count += 1;
|
|
100
|
+
if (isControlRoomSession(session)) project.liveCount += 1;
|
|
82
101
|
project.lastActivityAt = !project.lastActivityAt || Date.parse(session.updatedAt || 0) > Date.parse(project.lastActivityAt || 0)
|
|
83
102
|
? session.updatedAt
|
|
84
103
|
: project.lastActivityAt;
|
|
@@ -93,7 +112,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
93
112
|
item.name = parts.slice(-2).join("/") || item.name;
|
|
94
113
|
});
|
|
95
114
|
return items.sort((a, b) => Number(b.count || 0) - Number(a.count || 0)
|
|
96
|
-
|| Number(
|
|
115
|
+
|| Number(a.order ?? Number.MAX_SAFE_INTEGER) - Number(b.order ?? Number.MAX_SAFE_INTEGER)
|
|
97
116
|
|| String(a.name).localeCompare(String(b.name), uiLocale()));
|
|
98
117
|
}
|
|
99
118
|
|
|
@@ -130,24 +149,20 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
130
149
|
const rootSessions = displaySessions().filter((session) => !session.parentId);
|
|
131
150
|
const liveRootSessions = rootSessions.filter(isControlRoomSession);
|
|
132
151
|
const projects = observedProjects(rootSessions);
|
|
133
|
-
const
|
|
134
|
-
const liveProjects = observedProjects(liveRootSessions).sort((a, b) => {
|
|
135
|
-
const aOrder = savedProjectOrder.get(normalizedProjectPath(a.path));
|
|
136
|
-
const bOrder = savedProjectOrder.get(normalizedProjectPath(b.path));
|
|
137
|
-
if (aOrder != null || bOrder != null) return (aOrder ?? Number.MAX_SAFE_INTEGER) - (bOrder ?? Number.MAX_SAFE_INTEGER);
|
|
138
|
-
return Number(b.count || 0) - Number(a.count || 0) || String(a.name).localeCompare(String(b.name), uiLocale());
|
|
139
|
-
});
|
|
152
|
+
const liveProjects = observedProjects(liveRootSessions);
|
|
140
153
|
const projectlessCount = rootSessions.filter(isProjectlessSession).length;
|
|
141
154
|
const liveProjectlessCount = liveRootSessions.filter(isProjectlessSession).length;
|
|
142
155
|
const savedWorkspaceExists = state.workspace === "all"
|
|
143
156
|
|| (state.workspace === PROJECTLESS_WORKSPACE && projectlessCount > 0)
|
|
144
157
|
|| projects.some((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace));
|
|
145
158
|
if (!savedWorkspaceExists) state.workspace = "all";
|
|
146
|
-
const projectButton = (item, compactClass = "") => `<button type="button" class="workspace-item observed-project ${compactClass} ${state.workspace === item.path ? "selected" : ""}"
|
|
159
|
+
const projectButton = (item, compactClass = "") => `<button type="button" class="workspace-item observed-project ${compactClass} ${Number(item.liveCount || 0) ? "has-live-sessions" : ""} ${state.workspace === item.path ? "selected" : ""}"
|
|
147
160
|
data-workspace="${esc(item.path)}" title="${esc(item.path)}"
|
|
161
|
+
data-live-session-count="${Number(item.liveCount || 0)}"
|
|
148
162
|
aria-label="${esc(t("project.filter_named", { name: item.name, count: item.count }))}"
|
|
149
163
|
aria-pressed="${state.workspace === item.path ? "true" : "false"}">
|
|
150
164
|
<strong>${esc(item.name)}</strong><small>${Number(item.count || 0)}</small>
|
|
165
|
+
${Number(item.liveCount || 0) ? `<span class="workspace-live-state" title="${esc(t("project.in_progress"))}"><i aria-hidden="true"></i><b>${esc(t("project.in_progress"))}</b></span>` : ""}
|
|
151
166
|
</button>`;
|
|
152
167
|
const mobileHtml =
|
|
153
168
|
`<button type="button" class="workspace-item ${state.workspace === "all" ? "selected" : ""}"
|
|
@@ -551,6 +566,32 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
551
566
|
return true;
|
|
552
567
|
}
|
|
553
568
|
|
|
569
|
+
function ensureProjectOrder(projectKeys = []) {
|
|
570
|
+
if (!Array.isArray(state.projectOrder)) state.projectOrder = [];
|
|
571
|
+
const known = new Set(state.projectOrder);
|
|
572
|
+
for (const value of projectKeys) {
|
|
573
|
+
const key = String(value || "");
|
|
574
|
+
if (!key || known.has(key)) continue;
|
|
575
|
+
state.projectOrder.push(key);
|
|
576
|
+
known.add(key);
|
|
577
|
+
}
|
|
578
|
+
return state.projectOrder;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function moveProjectOrder(sourceId, targetId, placeAfter = false) {
|
|
582
|
+
const source = String(sourceId || "");
|
|
583
|
+
const target = String(targetId || "");
|
|
584
|
+
if (!source || !target || source === target) return false;
|
|
585
|
+
const order = ensureProjectOrder([source, target]);
|
|
586
|
+
const sourceIndex = order.indexOf(source);
|
|
587
|
+
if (sourceIndex < 0 || !order.includes(target)) return false;
|
|
588
|
+
order.splice(sourceIndex, 1);
|
|
589
|
+
const targetIndex = order.indexOf(target);
|
|
590
|
+
order.splice(targetIndex + (placeAfter ? 1 : 0), 0, source);
|
|
591
|
+
state.projectOrder = order;
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
|
|
554
595
|
function graphFilteredSessions() {
|
|
555
596
|
let sessions = displaySessions();
|
|
556
597
|
if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
|
|
@@ -607,6 +648,8 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
607
648
|
graphFilteredSessions,
|
|
608
649
|
stableSessionSort,
|
|
609
650
|
moveSessionOrder,
|
|
651
|
+
ensureProjectOrder,
|
|
652
|
+
moveProjectOrder,
|
|
610
653
|
renderProviderVisibilitySettings,
|
|
611
654
|
};
|
|
612
655
|
};
|
|
@@ -38,7 +38,7 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
38
38
|
if (message.role === "tool" && current && current.assistants.length) current.activityAfterAssistant = true;
|
|
39
39
|
}
|
|
40
40
|
const latestIndex = turns.length - 1;
|
|
41
|
-
const live = session.status === "running" || session.status === "starting";
|
|
41
|
+
const live = options.forceLatestLive || session.status === "running" || session.status === "starting";
|
|
42
42
|
return turns.map((turn, index) => ({
|
|
43
43
|
...turn,
|
|
44
44
|
representative: turn.assistants.at(-1) || null,
|
|
@@ -57,16 +57,66 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
57
57
|
const answerKind = assistant && options.answerKind
|
|
58
58
|
? `<span class="chat-answer-kind${options.live ? " is-live" : ""}">${esc(options.answerKind)}</span>`
|
|
59
59
|
: "";
|
|
60
|
-
|
|
60
|
+
const deliveryStatus = !assistant && message.deliveryStatus
|
|
61
|
+
? `<span class="chat-delivery-status ${esc(message.deliveryStatus)}">${esc(t(message.deliveryStatus === "failed"
|
|
62
|
+
? "drawer.message_failed"
|
|
63
|
+
: message.deliveryStatus === "sending"
|
|
64
|
+
? "drawer.message_sending"
|
|
65
|
+
: "drawer.message_sent"))}</span>`
|
|
66
|
+
: "";
|
|
67
|
+
const optimisticClasses = message.optimistic
|
|
68
|
+
? ` is-optimistic is-${esc(message.deliveryStatus || "awaiting")}${message.animate ? " is-new" : ""}`
|
|
69
|
+
: "";
|
|
70
|
+
return `<div class="chat-row ${assistant ? "assistant" : "user"}${optimisticClasses}" data-message-id="${esc(message.id || "")}">
|
|
61
71
|
<span class="chat-avatar">${esc(avatar)}</span>
|
|
62
72
|
<div class="chat-bubble">
|
|
63
73
|
<div class="chat-bubble-head">
|
|
64
74
|
<b>${esc(label)}</b>
|
|
65
|
-
<span title="${esc(fullTime)}">${esc(timeOnly(message.timestamp))}</span>${answerKind}
|
|
75
|
+
<span title="${esc(fullTime)}">${esc(timeOnly(message.timestamp))}</span>${deliveryStatus}${answerKind}
|
|
66
76
|
</div>${messageContentHtml(message, session.id)}</div>
|
|
67
77
|
</div>`;
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
function conversationOverlay(session) {
|
|
81
|
+
const pending = state.pendingConversationMessages.get(session.id) || [];
|
|
82
|
+
if (!pending.length) return { session, forceLatestLive: false };
|
|
83
|
+
const messages = [...(session.messages || [])];
|
|
84
|
+
const messageKey = (message) => {
|
|
85
|
+
const id = String(message?.id || "").trim();
|
|
86
|
+
if (id) return `id:${id}`;
|
|
87
|
+
return `${message?.role || ""}:${String(message?.text || "").replace(/\s+/g, " ").trim()}:${message?.timestamp || ""}`;
|
|
88
|
+
};
|
|
89
|
+
const normalizedText = (value) => String(value || "").replace(/\s+/g, " ").trim();
|
|
90
|
+
const retained = [];
|
|
91
|
+
let forceLatestLive = false;
|
|
92
|
+
for (const entry of pending) {
|
|
93
|
+
const newMessages = messages.filter(message => !entry.baselineMessageKeys?.has(messageKey(message)));
|
|
94
|
+
const actualUser = newMessages.find(message =>
|
|
95
|
+
message.role === "user" && normalizedText(message.text) === normalizedText(entry.text));
|
|
96
|
+
const newAssistant = newMessages.find(message => message.role === "assistant" && normalizedText(message.text));
|
|
97
|
+
if (actualUser && newAssistant) continue;
|
|
98
|
+
if (!actualUser) {
|
|
99
|
+
messages.push({
|
|
100
|
+
id: entry.id,
|
|
101
|
+
role: "user",
|
|
102
|
+
text: entry.text,
|
|
103
|
+
timestamp: entry.timestamp,
|
|
104
|
+
optimistic: true,
|
|
105
|
+
animate: !entry.presented,
|
|
106
|
+
deliveryStatus: entry.status,
|
|
107
|
+
});
|
|
108
|
+
entry.presented = true;
|
|
109
|
+
}
|
|
110
|
+
if (newAssistant) entry.status = "resolved";
|
|
111
|
+
if (entry.status === "sending" || entry.status === "awaiting") forceLatestLive = true;
|
|
112
|
+
retained.push(entry);
|
|
113
|
+
}
|
|
114
|
+
if (retained.length) state.pendingConversationMessages.set(session.id, retained);
|
|
115
|
+
else state.pendingConversationMessages.delete(session.id);
|
|
116
|
+
messages.sort((left, right) => Date.parse(left.timestamp || 0) - Date.parse(right.timestamp || 0));
|
|
117
|
+
return { session: { ...session, messages }, forceLatestLive };
|
|
118
|
+
}
|
|
119
|
+
|
|
70
120
|
function progressUpdatesHtml(turn, session) {
|
|
71
121
|
if (!turn.progress.length) return "";
|
|
72
122
|
const rows = turn.progress.map((message, index) => {
|
|
@@ -224,13 +274,15 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
224
274
|
}
|
|
225
275
|
|
|
226
276
|
function chatHtml(session, options = {}) {
|
|
277
|
+
const overlay = conversationOverlay(session);
|
|
278
|
+
session = overlay.session;
|
|
227
279
|
const messages = session.messages || [];
|
|
228
280
|
const calls = options.showSubagentCalls === false ? [] : subagentCallEvents(session);
|
|
229
281
|
if (!messages.length && !calls.length) return `<div class="empty-state"><h3>${esc(t("drawer.no_conversation"))}</h3></div>`;
|
|
230
282
|
const userLabel = options.userLabel || t("drawer.user");
|
|
231
283
|
const assistantLabel = options.assistantLabel || providerInfo(session.provider).label;
|
|
232
284
|
const conversationLabel = options.conversationLabel || t("drawer.conversation");
|
|
233
|
-
const turns = conversationTurns(session, options);
|
|
285
|
+
const turns = conversationTurns(session, { ...options, forceLatestLive: overlay.forceLatestLive });
|
|
234
286
|
const progressCount = turns.reduce((sum, turn) => sum + turn.progress.length, 0);
|
|
235
287
|
const omitted = Number(session.omittedMessages || 0);
|
|
236
288
|
const notice =
|
|
@@ -375,17 +427,31 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
375
427
|
const inheritedText = parentMessageKeys.has(messageKey(message));
|
|
376
428
|
return !inheritedId && !inheritedText;
|
|
377
429
|
});
|
|
430
|
+
const existingKeys = new Set(messages.map(messageKey));
|
|
431
|
+
for (const event of subagentCoordinationEvents(session)) {
|
|
432
|
+
if (!["followup", "message"].includes(event.kind) || !normalizedText(event.text)) continue;
|
|
433
|
+
const message = {
|
|
434
|
+
id: `${session.id}:coordination:${event.id}`,
|
|
435
|
+
role: "user",
|
|
436
|
+
text: event.text,
|
|
437
|
+
timestamp: event.timestamp || session.updatedAt,
|
|
438
|
+
source: "coordination",
|
|
439
|
+
};
|
|
440
|
+
const key = messageKey(message);
|
|
441
|
+
if (existingKeys.has(key)) continue;
|
|
442
|
+
messages.push(message);
|
|
443
|
+
existingKeys.add(key);
|
|
444
|
+
}
|
|
378
445
|
const hasConversation = messages.some((message) =>
|
|
379
446
|
(message.role === "user" || message.role === "assistant") && String(message.text || "").trim(),
|
|
380
447
|
);
|
|
381
|
-
if (hasConversation)
|
|
382
|
-
if (delegation.assignmentObserved && !delegation.assignmentProtected && String(delegation.assignment || "").trim()) {
|
|
448
|
+
if (!hasConversation && delegation.assignmentObserved && !delegation.assignmentProtected && String(delegation.assignment || "").trim()) {
|
|
383
449
|
messages.push({
|
|
384
450
|
id: `${session.id}:delegation`, role: "user", text: delegation.assignment,
|
|
385
451
|
timestamp: delegation.startedAt || session.startedAt || session.updatedAt,
|
|
386
452
|
});
|
|
387
453
|
}
|
|
388
|
-
if (String(session.result || delegation.result || "").trim()) {
|
|
454
|
+
if (!hasConversation && String(session.result || delegation.result || "").trim()) {
|
|
389
455
|
messages.push({
|
|
390
456
|
id: `${session.id}:result`, role: "assistant", text: session.result || delegation.result,
|
|
391
457
|
timestamp: session.completedAt || delegation.completedAt || session.updatedAt,
|
|
@@ -515,5 +581,9 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
515
581
|
</div>`;
|
|
516
582
|
}
|
|
517
583
|
|
|
518
|
-
return {
|
|
584
|
+
return {
|
|
585
|
+
conversationTurns, chatHtml, lifecycleHtml, tokensHtml,
|
|
586
|
+
subagentCommunicationEvents, subagentCoordinationEvents, subagentWorkMessages,
|
|
587
|
+
subagentTextPreview, subagentConversationHtml, executionActivityDetailHtml,
|
|
588
|
+
};
|
|
519
589
|
};
|
|
@@ -4,7 +4,7 @@ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
|
4
4
|
|
|
5
5
|
window.LoadToAgentAppFactories.createFilterEventBindings = function createFilterEventBindings(context = {}) {
|
|
6
6
|
const t = (key, params) => window.LoadToAgentI18n.t(key, params);
|
|
7
|
-
const { $, state, setProviderVisible = () => {}, visibleSnapshot = () => state.snapshot, closeDrawer = () => {}, openDrawer = () => {}, renderSessions, render, renderWorkspaces, renderProviderOverview, renderProviderFilter, toggleProviderFilter, announceProviderFilter, filteredSessions, performUiAction, toast, announce, normalizedSearch = (value) => String(value || "").trim(), saveDashboardPreferences = () => {}, restoreDialogTrigger = () => {}, setDialogOpenState = () => {} } = context;
|
|
7
|
+
const { $, state, setProviderVisible = () => {}, visibleSnapshot = () => state.snapshot, closeDrawer = () => {}, openDrawer = () => {}, renderSessions, render, renderWorkspaces, renderProviderOverview, renderProviderFilter, toggleProviderFilter, announceProviderFilter, filteredSessions, performUiAction, toast, announce, normalizedSearch = (value) => String(value || "").trim(), saveDashboardPreferences = () => {}, restoreDialogTrigger = () => {}, setDialogOpenState = () => {}, syncControlRoomDisclosureButtons = () => {} } = context;
|
|
8
8
|
|
|
9
9
|
function bindFilterAndWorkspaceEvents() {
|
|
10
10
|
const syncFilterResetButton = () => {
|
|
@@ -62,7 +62,6 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
62
62
|
if (item) {
|
|
63
63
|
const label = item.querySelector("strong")?.textContent.trim() || t("project.all");
|
|
64
64
|
state.workspace = item.dataset.workspace;
|
|
65
|
-
state.controlRoomPage = 0;
|
|
66
65
|
state.visibleLimit = 30;
|
|
67
66
|
renderWorkspaces();
|
|
68
67
|
renderSessions("filter");
|
|
@@ -74,9 +73,12 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
74
73
|
setDialogOpenState(menu, false);
|
|
75
74
|
menu?.classList.add("hidden");
|
|
76
75
|
$("#mobileMoreBtn")?.setAttribute("aria-expanded", "false");
|
|
77
|
-
const focusResults = () =>
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
const focusResults = () => {
|
|
77
|
+
const result = $("#liveSessionGrid")?.querySelector("[data-graph-focus], [data-open-session]")
|
|
78
|
+
|| $("#sessionGrid")?.querySelector("[data-session-id]");
|
|
79
|
+
result?.focus({ preventScroll: true });
|
|
80
|
+
if (document.activeElement !== result) $("#mainContent")?.focus({ preventScroll: true });
|
|
81
|
+
};
|
|
80
82
|
restoreDialogTrigger();
|
|
81
83
|
focusResults();
|
|
82
84
|
requestAnimationFrame(focusResults);
|
|
@@ -93,7 +95,6 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
93
95
|
const controlProjectSelect = $("#controlRoomProjectSelect");
|
|
94
96
|
controlProjectSelect?.addEventListener("change", (event) => {
|
|
95
97
|
state.workspace = event.target.value;
|
|
96
|
-
state.controlRoomPage = 0;
|
|
97
98
|
state.visibleLimit = 30;
|
|
98
99
|
renderWorkspaces();
|
|
99
100
|
renderSessions("filter");
|
|
@@ -104,7 +105,6 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
104
105
|
const controlSortSelect = $("#controlRoomSortSelect");
|
|
105
106
|
controlSortSelect?.addEventListener("change", (event) => {
|
|
106
107
|
state.controlRoomSort = event.target.value;
|
|
107
|
-
state.controlRoomPage = 0;
|
|
108
108
|
state.visibleLimit = 30;
|
|
109
109
|
renderSessions("filter");
|
|
110
110
|
syncFilterResetButton();
|
|
@@ -131,7 +131,6 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
131
131
|
if ($("#searchInput")) $("#searchInput").value = value;
|
|
132
132
|
controlSearchTimer = setTimeout(() => {
|
|
133
133
|
state.search = normalizedSearch(value);
|
|
134
|
-
state.controlRoomPage = 0;
|
|
135
134
|
state.visibleLimit = 30;
|
|
136
135
|
renderSessions("filter");
|
|
137
136
|
syncFilterResetButton();
|
|
@@ -150,16 +149,18 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
150
149
|
controlSearchButton?.focus();
|
|
151
150
|
}
|
|
152
151
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
152
|
+
const setAllControlRoomGroups = (open) => {
|
|
153
|
+
document.querySelectorAll("#liveSessionGrid .control-room-project-group").forEach(group => {
|
|
154
|
+
group.open = open;
|
|
155
|
+
});
|
|
156
|
+
syncControlRoomDisclosureButtons();
|
|
157
|
+
announce(t(open ? "control.all_projects_expanded" : "control.all_projects_collapsed"));
|
|
158
|
+
};
|
|
159
|
+
$("#controlRoomExpandAll")?.addEventListener("click", () => setAllControlRoomGroups(true));
|
|
160
|
+
$("#controlRoomCollapseAll")?.addEventListener("click", () => setAllControlRoomGroups(false));
|
|
161
|
+
$("#liveSessionGrid")?.addEventListener("toggle", (event) => {
|
|
162
|
+
if (event.target.matches?.(".control-room-project-group")) syncControlRoomDisclosureButtons();
|
|
163
|
+
}, true);
|
|
163
164
|
let searchTimer = null;
|
|
164
165
|
$("#searchInput").addEventListener("input", (event) => {
|
|
165
166
|
clearTimeout(searchTimer);
|
|
@@ -168,7 +169,6 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
168
169
|
syncFilterResetButton();
|
|
169
170
|
searchTimer = setTimeout(() => {
|
|
170
171
|
state.search = normalizedSearch(value);
|
|
171
|
-
state.controlRoomPage = 0;
|
|
172
172
|
state.visibleLimit = 30;
|
|
173
173
|
renderSessions("filter");
|
|
174
174
|
announce(window.LoadToAgentI18n.t("filter.search_results", { count: filteredSessions().length }));
|
|
@@ -181,7 +181,6 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
181
181
|
$("#searchInput").value = "";
|
|
182
182
|
$("#searchClearBtn").classList.add("hidden");
|
|
183
183
|
state.search = "";
|
|
184
|
-
state.controlRoomPage = 0;
|
|
185
184
|
state.visibleLimit = 30;
|
|
186
185
|
renderSessions("filter");
|
|
187
186
|
announce(window.LoadToAgentI18n.t("filter.search_cleared"));
|
|
@@ -212,7 +211,6 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
212
211
|
const chip = event.target.closest("[data-provider-filter]");
|
|
213
212
|
if (!chip) return;
|
|
214
213
|
toggleProviderFilter(chip.dataset.providerFilter);
|
|
215
|
-
state.controlRoomPage = 0;
|
|
216
214
|
state.visibleLimit = 30;
|
|
217
215
|
renderProviderFilter();
|
|
218
216
|
renderProviderOverview();
|
|
@@ -229,7 +227,6 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
229
227
|
});
|
|
230
228
|
$("#sortSelect").addEventListener("change", (event) => {
|
|
231
229
|
state.sort = event.target.value;
|
|
232
|
-
state.controlRoomPage = 0;
|
|
233
230
|
state.visibleLimit = 30;
|
|
234
231
|
renderSessions("filter");
|
|
235
232
|
const label = event.target.selectedOptions[0]?.textContent || event.target.value;
|
|
@@ -244,7 +241,6 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
244
241
|
state.workspace = "all";
|
|
245
242
|
state.sort = "recent";
|
|
246
243
|
state.controlRoomSort = "recent";
|
|
247
|
-
state.controlRoomPage = 0;
|
|
248
244
|
state.visibleLimit = 30;
|
|
249
245
|
$("#searchInput").value = "";
|
|
250
246
|
$("#searchClearBtn").classList.add("hidden");
|