loadtoagent 1.3.7 → 1.3.9
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 +13 -1
- package/README.md +13 -1
- package/README.zh-CN.md +13 -1
- package/main.js +3 -2
- package/package.json +2 -1
- package/renderer/app-agent-actions.js +73 -22
- package/renderer/app-dashboard.js +56 -13
- package/renderer/app-drawer-content.js +152 -38
- package/renderer/app-drawer.js +13 -2
- 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 +49 -16
- package/renderer/app-quality.js +5 -0
- package/renderer/app.js +69 -5
- package/renderer/conversation-delivery.js +74 -0
- package/renderer/i18n-messages.js +58 -1
- package/renderer/index.html +5 -6
- package/renderer/styles-components.css +240 -62
- package/renderer/styles-control-room.css +64 -43
- package/renderer/styles-responsive-shell.css +6 -2
- 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
|
@@ -6,6 +6,7 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
6
6
|
const {
|
|
7
7
|
esc, uiLocale, state, messageContentHtml, compact, fullNumber, timeOnly, providerInfo, statusIcon, agentPathTaskName, snapshotSession,
|
|
8
8
|
controlRoomAgentGoal, inferredExecutionSummary, executionActivityLabel, executionActivityStatus,
|
|
9
|
+
conversationDeliveryState, observeConversationDelivery,
|
|
9
10
|
} = context;
|
|
10
11
|
const t = (key, params) => window.LoadToAgentI18n.t(key, params);
|
|
11
12
|
|
|
@@ -38,7 +39,7 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
38
39
|
if (message.role === "tool" && current && current.assistants.length) current.activityAfterAssistant = true;
|
|
39
40
|
}
|
|
40
41
|
const latestIndex = turns.length - 1;
|
|
41
|
-
const live = session.status === "running" || session.status === "starting";
|
|
42
|
+
const live = options.forceLatestLive || session.status === "running" || session.status === "starting";
|
|
42
43
|
return turns.map((turn, index) => ({
|
|
43
44
|
...turn,
|
|
44
45
|
representative: turn.assistants.at(-1) || null,
|
|
@@ -57,31 +58,81 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
57
58
|
const answerKind = assistant && options.answerKind
|
|
58
59
|
? `<span class="chat-answer-kind${options.live ? " is-live" : ""}">${esc(options.answerKind)}</span>`
|
|
59
60
|
: "";
|
|
60
|
-
|
|
61
|
+
const deliveryStatusKey = {
|
|
62
|
+
sending: "drawer.message_sending",
|
|
63
|
+
confirming: "drawer.message_confirming",
|
|
64
|
+
delayed: "drawer.message_unconfirmed",
|
|
65
|
+
received: "drawer.message_received",
|
|
66
|
+
responding: "drawer.message_responding",
|
|
67
|
+
failed: "drawer.message_failed",
|
|
68
|
+
};
|
|
69
|
+
const deliveryStatus = !assistant && message.deliveryStatus
|
|
70
|
+
? `<span class="chat-delivery-status ${esc(message.deliveryStatus)}">${esc(t(deliveryStatusKey[message.deliveryStatus]
|
|
71
|
+
|| "drawer.message_sent"))}</span>`
|
|
72
|
+
: "";
|
|
73
|
+
const optimisticClasses = message.optimistic
|
|
74
|
+
? ` is-optimistic is-${esc(message.deliveryStatus || "awaiting")}${message.animate ? " is-new" : ""}`
|
|
75
|
+
: "";
|
|
76
|
+
return `<div class="chat-row ${assistant ? "assistant" : "user"}${optimisticClasses}" data-message-id="${esc(message.id || "")}">
|
|
61
77
|
<span class="chat-avatar">${esc(avatar)}</span>
|
|
62
78
|
<div class="chat-bubble">
|
|
63
79
|
<div class="chat-bubble-head">
|
|
64
80
|
<b>${esc(label)}</b>
|
|
65
|
-
<span title="${esc(fullTime)}">${esc(timeOnly(message.timestamp))}</span>${answerKind}
|
|
81
|
+
<span title="${esc(fullTime)}">${esc(timeOnly(message.timestamp))}</span>${deliveryStatus}${answerKind}
|
|
66
82
|
</div>${messageContentHtml(message, session.id)}</div>
|
|
67
83
|
</div>`;
|
|
68
84
|
}
|
|
69
85
|
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
function conversationOverlay(session) {
|
|
87
|
+
const pending = state.pendingConversationMessages.get(session.id) || [];
|
|
88
|
+
if (!pending.length) return { session, forceLatestLive: false };
|
|
89
|
+
const messages = [...(session.messages || [])];
|
|
90
|
+
const retained = [];
|
|
91
|
+
let forceLatestLive = false;
|
|
92
|
+
for (const entry of pending) {
|
|
93
|
+
const delivery = conversationDeliveryState({ ...session, messages }, entry);
|
|
94
|
+
if (!delivery) continue;
|
|
95
|
+
observeConversationDelivery(session, entry, delivery);
|
|
96
|
+
if (delivery.phase === "responded") {
|
|
97
|
+
clearTimeout(entry.confirmationTimer);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const tracking = {
|
|
101
|
+
phase: delivery.phase,
|
|
102
|
+
elapsedMs: delivery.elapsedMs,
|
|
103
|
+
error: entry.error || "",
|
|
104
|
+
receivedAt: delivery.receivedAt,
|
|
105
|
+
responseObservedAt: delivery.responseObservedAt,
|
|
106
|
+
};
|
|
107
|
+
if (!delivery.userMessage) {
|
|
108
|
+
messages.push({
|
|
109
|
+
id: entry.id,
|
|
110
|
+
role: "user",
|
|
111
|
+
text: entry.text,
|
|
112
|
+
timestamp: entry.timestamp,
|
|
113
|
+
optimistic: true,
|
|
114
|
+
animate: !entry.presented,
|
|
115
|
+
deliveryStatus: delivery.phase,
|
|
116
|
+
deliveryTracking: tracking,
|
|
117
|
+
});
|
|
118
|
+
entry.presented = true;
|
|
119
|
+
} else {
|
|
120
|
+
const actualIndex = messages.indexOf(delivery.userMessage);
|
|
121
|
+
if (actualIndex >= 0) {
|
|
122
|
+
messages[actualIndex] = {
|
|
123
|
+
...delivery.userMessage,
|
|
124
|
+
deliveryStatus: delivery.phase,
|
|
125
|
+
deliveryTracking: tracking,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
forceLatestLive = true;
|
|
130
|
+
retained.push(entry);
|
|
131
|
+
}
|
|
132
|
+
if (retained.length) state.pendingConversationMessages.set(session.id, retained);
|
|
133
|
+
else state.pendingConversationMessages.delete(session.id);
|
|
134
|
+
messages.sort((left, right) => Date.parse(left.timestamp || 0) - Date.parse(right.timestamp || 0));
|
|
135
|
+
return { session: { ...session, messages }, forceLatestLive };
|
|
85
136
|
}
|
|
86
137
|
|
|
87
138
|
function subagentCallEvents(session) {
|
|
@@ -211,27 +262,80 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
211
262
|
});
|
|
212
263
|
return items.map(item => {
|
|
213
264
|
if (item.kind === "call") return subagentCallHtml(item.call, { showAnchor: false });
|
|
214
|
-
const finalMessage = item.
|
|
265
|
+
const finalMessage = item.index === turn.assistants.length - 1
|
|
266
|
+
|| (representativeId && item.message.id === representativeId);
|
|
215
267
|
return conversationRowHtml(item.message, session, {
|
|
216
268
|
userLabel: labels.userLabel,
|
|
217
269
|
assistantLabel: labels.assistantLabel,
|
|
218
270
|
answerKind: finalMessage
|
|
219
271
|
? t(turn.live ? "drawer.current_progress" : turn.awaitingFinal ? "drawer.last_progress" : "drawer.final_answer")
|
|
220
|
-
:
|
|
272
|
+
: "",
|
|
221
273
|
live: turn.live && finalMessage,
|
|
222
274
|
});
|
|
223
275
|
}).join("");
|
|
224
276
|
}
|
|
225
277
|
|
|
278
|
+
function deliveryProgressHtml(tracking) {
|
|
279
|
+
if (!tracking) {
|
|
280
|
+
return `<div class="chat-delivery-progress phase-observed" data-delivery-phase="observed" role="status" aria-live="polite">
|
|
281
|
+
<header><span aria-hidden="true"></span><div><b>${esc(t("drawer.delivery_observed_title"))}</b><small>${esc(t("drawer.delivery_observed_detail"))}</small></div></header>
|
|
282
|
+
<ol>
|
|
283
|
+
<li class="done"><i aria-hidden="true">✓</i><span><b>${esc(t("drawer.delivery_observed_message"))}</b><small>${esc(t("drawer.delivery_observed_message_help"))}</small></span></li>
|
|
284
|
+
<li class="active"><i aria-hidden="true">2</i><span><b>${esc(t("drawer.delivery_observed_session"))}</b><small>${esc(t("drawer.delivery_observed_session_help"))}</small></span></li>
|
|
285
|
+
</ol>
|
|
286
|
+
<footer><i aria-hidden="true">◎</i><span>${esc(t("drawer.delivery_evidence_observed"))}</span></footer>
|
|
287
|
+
</div>`;
|
|
288
|
+
}
|
|
289
|
+
const phase = tracking.phase || "confirming";
|
|
290
|
+
const copy = {
|
|
291
|
+
sending: ["drawer.delivery_sending_title", "drawer.delivery_sending_detail", "drawer.delivery_evidence_sending"],
|
|
292
|
+
confirming: ["drawer.delivery_confirming_title", "drawer.delivery_confirming_detail", "drawer.delivery_evidence_confirming"],
|
|
293
|
+
delayed: ["drawer.delivery_delayed_title", "drawer.delivery_delayed_detail", "drawer.delivery_evidence_delayed"],
|
|
294
|
+
received: ["drawer.delivery_received_title", "drawer.delivery_received_detail", "drawer.delivery_evidence_received"],
|
|
295
|
+
responding: ["drawer.delivery_responding_title", "drawer.delivery_responding_detail", "drawer.delivery_evidence_responding"],
|
|
296
|
+
failed: ["drawer.delivery_failed_title", "drawer.delivery_failed_detail", "drawer.delivery_evidence_failed"],
|
|
297
|
+
}[phase] || ["drawer.delivery_confirming_title", "drawer.delivery_confirming_detail", "drawer.delivery_evidence_confirming"];
|
|
298
|
+
const stepState = (step) => {
|
|
299
|
+
if (phase === "failed") return step === 0 ? "error" : "pending";
|
|
300
|
+
if (phase === "sending") return step === 0 ? "active" : "pending";
|
|
301
|
+
if (phase === "confirming" || phase === "delayed") {
|
|
302
|
+
if (step === 0) return "done";
|
|
303
|
+
if (step === 1) return phase === "delayed" ? "warning" : "active";
|
|
304
|
+
return "pending";
|
|
305
|
+
}
|
|
306
|
+
if (phase === "received") return step < 2 ? "done" : "active";
|
|
307
|
+
if (phase === "responding") return step < 2 ? "done" : "active";
|
|
308
|
+
return "pending";
|
|
309
|
+
};
|
|
310
|
+
const steps = [
|
|
311
|
+
["drawer.delivery_step_dispatch", "drawer.delivery_step_dispatch_help"],
|
|
312
|
+
["drawer.delivery_step_received", "drawer.delivery_step_received_help"],
|
|
313
|
+
["drawer.delivery_step_response", "drawer.delivery_step_response_help"],
|
|
314
|
+
].map(([label, detail], index) => {
|
|
315
|
+
const status = stepState(index);
|
|
316
|
+
const marker = status === "done" ? "✓" : status === "error" ? "!" : String(index + 1);
|
|
317
|
+
return `<li class="${status}"><i aria-hidden="true">${marker}</i><span><b>${esc(t(label))}</b><small>${esc(t(detail))}</small></span></li>`;
|
|
318
|
+
}).join("");
|
|
319
|
+
const detail = phase === "failed" && tracking.error
|
|
320
|
+
? t("drawer.delivery_failed_with_reason", { reason: tracking.error })
|
|
321
|
+
: t(copy[1]);
|
|
322
|
+
return `<div class="chat-delivery-progress phase-${esc(phase)}" data-delivery-phase="${esc(phase)}" role="status" aria-live="polite">
|
|
323
|
+
<header><span aria-hidden="true"></span><div><b>${esc(t(copy[0]))}</b><small>${esc(detail)}</small></div></header>
|
|
324
|
+
<ol>${steps}</ol>
|
|
325
|
+
<footer><i aria-hidden="true">◎</i><span>${esc(t(copy[2]))}</span></footer>
|
|
326
|
+
</div>`;
|
|
327
|
+
}
|
|
328
|
+
|
|
226
329
|
function chatHtml(session, options = {}) {
|
|
330
|
+
const overlay = conversationOverlay(session);
|
|
331
|
+
session = overlay.session;
|
|
227
332
|
const messages = session.messages || [];
|
|
228
333
|
const calls = options.showSubagentCalls === false ? [] : subagentCallEvents(session);
|
|
229
334
|
if (!messages.length && !calls.length) return `<div class="empty-state"><h3>${esc(t("drawer.no_conversation"))}</h3></div>`;
|
|
230
335
|
const userLabel = options.userLabel || t("drawer.user");
|
|
231
336
|
const assistantLabel = options.assistantLabel || providerInfo(session.provider).label;
|
|
232
337
|
const conversationLabel = options.conversationLabel || t("drawer.conversation");
|
|
233
|
-
const turns = conversationTurns(session, options);
|
|
234
|
-
const progressCount = turns.reduce((sum, turn) => sum + turn.progress.length, 0);
|
|
338
|
+
const turns = conversationTurns(session, { ...options, forceLatestLive: overlay.forceLatestLive });
|
|
235
339
|
const omitted = Number(session.omittedMessages || 0);
|
|
236
340
|
const notice =
|
|
237
341
|
omitted || session.truncated
|
|
@@ -239,14 +343,8 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
239
343
|
: "";
|
|
240
344
|
const rows = turns.map((turn, turnIndex) => {
|
|
241
345
|
const user = conversationRowHtml(turn.user, session, { userLabel, assistantLabel });
|
|
242
|
-
const representative = conversationRowHtml(turn.representative, session, {
|
|
243
|
-
userLabel,
|
|
244
|
-
assistantLabel,
|
|
245
|
-
answerKind: t(turn.live ? "drawer.current_progress" : turn.awaitingFinal ? "drawer.last_progress" : "drawer.final_answer"),
|
|
246
|
-
live: turn.live,
|
|
247
|
-
});
|
|
248
346
|
const waiting = turn.live && !turn.representative
|
|
249
|
-
?
|
|
347
|
+
? deliveryProgressHtml(turn.user?.deliveryTracking)
|
|
250
348
|
: "";
|
|
251
349
|
const turnStartedAt = Date.parse(turn.user?.timestamp || turn.representative?.timestamp || 0);
|
|
252
350
|
const nextTurnTimestamp = turns[turnIndex + 1]?.user?.timestamp;
|
|
@@ -256,11 +354,9 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
256
354
|
if (Number.isFinite(turnStartedAt) && calledAt < turnStartedAt) return false;
|
|
257
355
|
return !Number.isFinite(nextTurnStartedAt) || calledAt < nextTurnStartedAt;
|
|
258
356
|
});
|
|
259
|
-
const timeline = turnCalls
|
|
260
|
-
? turnWithSubagentCallsHtml(turn, session, turnCalls, { userLabel, assistantLabel })
|
|
261
|
-
: `${representative}${waiting}${progressUpdatesHtml(turn, session)}`;
|
|
357
|
+
const timeline = turnWithSubagentCallsHtml(turn, session, turnCalls, { userLabel, assistantLabel });
|
|
262
358
|
return `<section class="chat-turn${turn.live ? " is-live" : ""}" data-conversation-turn="${esc(turn.id)}">
|
|
263
|
-
${user}${timeline}${
|
|
359
|
+
${user}${timeline}${waiting}
|
|
264
360
|
</section>`;
|
|
265
361
|
}).join("");
|
|
266
362
|
const unmatchedCalls = calls.filter(call => !turns.some((turn, turnIndex) => {
|
|
@@ -273,7 +369,7 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
273
369
|
const callOnlyRows = unmatchedCalls.map(subagentCallHtml).join("");
|
|
274
370
|
const emptyConversation = turns.length || calls.length ? "" : `<div class="empty-state compact"><h3>${esc(t("drawer.no_user_ai_conversation"))}</h3></div>`;
|
|
275
371
|
return `${notice}<div class="chat-history-head">
|
|
276
|
-
<span>${esc(t("drawer.turn_summary", { label: conversationLabel, count: turns.length, updates:
|
|
372
|
+
<span>${esc(t("drawer.turn_summary", { label: conversationLabel, count: turns.length, updates: "" }))}</span>
|
|
277
373
|
<button type="button" data-scroll-latest>${esc(t("drawer.latest_conversation"))} ↓</button>
|
|
278
374
|
</div>
|
|
279
375
|
<div class="chat-list">${callOnlyRows}${rows}${emptyConversation}<div class="chat-latest-anchor" aria-label="${esc(t("drawer.latest_conversation"))}">
|
|
@@ -375,17 +471,31 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
375
471
|
const inheritedText = parentMessageKeys.has(messageKey(message));
|
|
376
472
|
return !inheritedId && !inheritedText;
|
|
377
473
|
});
|
|
474
|
+
const existingKeys = new Set(messages.map(messageKey));
|
|
475
|
+
for (const event of subagentCoordinationEvents(session)) {
|
|
476
|
+
if (!["followup", "message"].includes(event.kind) || !normalizedText(event.text)) continue;
|
|
477
|
+
const message = {
|
|
478
|
+
id: `${session.id}:coordination:${event.id}`,
|
|
479
|
+
role: "user",
|
|
480
|
+
text: event.text,
|
|
481
|
+
timestamp: event.timestamp || session.updatedAt,
|
|
482
|
+
source: "coordination",
|
|
483
|
+
};
|
|
484
|
+
const key = messageKey(message);
|
|
485
|
+
if (existingKeys.has(key)) continue;
|
|
486
|
+
messages.push(message);
|
|
487
|
+
existingKeys.add(key);
|
|
488
|
+
}
|
|
378
489
|
const hasConversation = messages.some((message) =>
|
|
379
490
|
(message.role === "user" || message.role === "assistant") && String(message.text || "").trim(),
|
|
380
491
|
);
|
|
381
|
-
if (hasConversation)
|
|
382
|
-
if (delegation.assignmentObserved && !delegation.assignmentProtected && String(delegation.assignment || "").trim()) {
|
|
492
|
+
if (!hasConversation && delegation.assignmentObserved && !delegation.assignmentProtected && String(delegation.assignment || "").trim()) {
|
|
383
493
|
messages.push({
|
|
384
494
|
id: `${session.id}:delegation`, role: "user", text: delegation.assignment,
|
|
385
495
|
timestamp: delegation.startedAt || session.startedAt || session.updatedAt,
|
|
386
496
|
});
|
|
387
497
|
}
|
|
388
|
-
if (String(session.result || delegation.result || "").trim()) {
|
|
498
|
+
if (!hasConversation && String(session.result || delegation.result || "").trim()) {
|
|
389
499
|
messages.push({
|
|
390
500
|
id: `${session.id}:result`, role: "assistant", text: session.result || delegation.result,
|
|
391
501
|
timestamp: session.completedAt || delegation.completedAt || session.updatedAt,
|
|
@@ -515,5 +625,9 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
515
625
|
</div>`;
|
|
516
626
|
}
|
|
517
627
|
|
|
518
|
-
return {
|
|
628
|
+
return {
|
|
629
|
+
conversationTurns, chatHtml, lifecycleHtml, tokensHtml,
|
|
630
|
+
subagentCommunicationEvents, subagentCoordinationEvents, subagentWorkMessages,
|
|
631
|
+
subagentTextPreview, subagentConversationHtml, executionActivityDetailHtml,
|
|
632
|
+
};
|
|
519
633
|
};
|
package/renderer/app-drawer.js
CHANGED
|
@@ -7,11 +7,20 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
7
7
|
const {
|
|
8
8
|
$, $$, esc, state, motionPreference, motionState, STATUS, markGuideStep, rememberDialogTrigger, restoreDialogTrigger, setDialogOpenState,
|
|
9
9
|
providerInfo, isLiveSession, controlRoomStatus = session => session?.status, subagentWorkState, subagentWorkLabel, isProjectlessSession, sessionOriginPath, sessionWorkspaceLabel,
|
|
10
|
+
pendingConversationDelivery = () => null,
|
|
10
11
|
agentResumeSupport, originAppInfo, selectedSession, snapshotSession, loadSessionDetail, loadSubagentParentDetail,
|
|
11
12
|
chatHtml, lifecycleHtml, tokensHtml, outcomeHtml, subagentCoordinationEvents, subagentConversationHtml, executionActivityDetailHtml,
|
|
12
13
|
agentCommandComposer,
|
|
13
14
|
rememberDisclosureStates = () => {}, restoreDisclosureStates = () => {},
|
|
14
15
|
} = context;
|
|
16
|
+
const deliveryLabelKey = (phase) => ({
|
|
17
|
+
sending: "control.delivery_sending",
|
|
18
|
+
confirming: "control.delivery_confirming",
|
|
19
|
+
delayed: "control.delivery_delayed",
|
|
20
|
+
received: "control.delivery_received",
|
|
21
|
+
responding: "control.delivery_responding",
|
|
22
|
+
failed: "control.delivery_failed",
|
|
23
|
+
})[phase] || "control.delivery_confirming";
|
|
15
24
|
|
|
16
25
|
function openDrawer(id) {
|
|
17
26
|
rememberDialogTrigger();
|
|
@@ -98,6 +107,8 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
98
107
|
if (!session) return closeDrawer();
|
|
99
108
|
const provider = providerInfo(session.provider);
|
|
100
109
|
const presentationStatus = controlRoomStatus(session);
|
|
110
|
+
const delivery = pendingConversationDelivery(session);
|
|
111
|
+
const presentationLabel = delivery ? t(deliveryLabelKey(delivery.phase)) : STATUS[presentationStatus] || presentationStatus;
|
|
101
112
|
const subagentMode = state.drawerMode === "subagent" && Boolean(session.parentId);
|
|
102
113
|
const executionMode = state.drawerMode === "execution" && Boolean(state.drawerExecutionId);
|
|
103
114
|
const snapshot = snapshotSession(session.id);
|
|
@@ -116,8 +127,8 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
116
127
|
$("#drawerProvider").textContent = executionMode
|
|
117
128
|
? `${activity?.runtime || activity?.tool || t("drawer.execution_unit")} · ${activity ? context.executionActivityStatus(activity) : t("drawer.unknown")}`
|
|
118
129
|
: subagentMode
|
|
119
|
-
? `${t("control.subagent")} · ${
|
|
120
|
-
: `${provider.company} · ${
|
|
130
|
+
? `${t("control.subagent")} · ${presentationLabel}`
|
|
131
|
+
: `${provider.company} · ${presentationLabel}`;
|
|
121
132
|
const drawerTitle = executionMode
|
|
122
133
|
? context.inferredExecutionSummary(activity || {}).text
|
|
123
134
|
: subagentMode ? session.title || session.taskName || (session.delegation && session.delegation.taskName) : session.title;
|
|
@@ -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");
|
|
@@ -10,10 +10,12 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
10
10
|
copyText = async () => false,
|
|
11
11
|
announce = () => {},
|
|
12
12
|
moveSessionOrder = () => false,
|
|
13
|
+
moveProjectOrder = () => false,
|
|
13
14
|
archiveSession = () => false,
|
|
14
15
|
} = context;
|
|
15
16
|
|
|
16
17
|
let sessionDragJustEnded = false;
|
|
18
|
+
let projectDragEndedAt = 0;
|
|
17
19
|
|
|
18
20
|
const sortableSessionId = node => String(node?.dataset.sessionSortable || "");
|
|
19
21
|
const clearSessionDropState = container => {
|
|
@@ -118,6 +120,96 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
118
120
|
});
|
|
119
121
|
};
|
|
120
122
|
|
|
123
|
+
const bindSortableProjectGroups = (container) => {
|
|
124
|
+
const selector = ".control-room-project-group[data-project-sortable]";
|
|
125
|
+
let draggedProjectId = "";
|
|
126
|
+
const projectId = node => String(node?.dataset.projectSortable || "");
|
|
127
|
+
const clearProjectDropState = () => {
|
|
128
|
+
container.querySelectorAll(selector).forEach(group => {
|
|
129
|
+
group.classList.remove("project-sort-dragging");
|
|
130
|
+
group.removeAttribute("data-project-drop-edge");
|
|
131
|
+
group.querySelector(":scope > .control-project-header")?.setAttribute("aria-grabbed", "false");
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
const finishProjectDrag = () => {
|
|
135
|
+
clearProjectDropState();
|
|
136
|
+
draggedProjectId = "";
|
|
137
|
+
projectDragEndedAt = Date.now();
|
|
138
|
+
};
|
|
139
|
+
const commitProjectPosition = (sourceId, targetId, placeAfter, focusSource = false) => {
|
|
140
|
+
if (!moveProjectOrder(sourceId, targetId, placeAfter)) return false;
|
|
141
|
+
saveDashboardPreferences();
|
|
142
|
+
renderSessions("reorder");
|
|
143
|
+
announce(window.LoadToAgentI18n.t("project.position_changed"));
|
|
144
|
+
if (focusSource) {
|
|
145
|
+
requestAnimationFrame(() => container
|
|
146
|
+
.querySelector(`${selector}[data-project-sortable="${CSS.escape(sourceId)}"] > .control-project-header`)
|
|
147
|
+
?.focus({ preventScroll: true }));
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
};
|
|
151
|
+
container.addEventListener("dragstart", (event) => {
|
|
152
|
+
const header = event.target.closest(".control-project-header[draggable='true']");
|
|
153
|
+
const group = header?.closest(selector);
|
|
154
|
+
if (!group) return;
|
|
155
|
+
draggedProjectId = projectId(group);
|
|
156
|
+
if (!draggedProjectId) {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
group.classList.add("project-sort-dragging");
|
|
161
|
+
header.setAttribute("aria-grabbed", "true");
|
|
162
|
+
if (event.dataTransfer) {
|
|
163
|
+
event.dataTransfer.effectAllowed = "move";
|
|
164
|
+
event.dataTransfer.setData("text/plain", draggedProjectId);
|
|
165
|
+
event.dataTransfer.setData("application/x-loadtoagent-project-list", container.id);
|
|
166
|
+
event.dataTransfer.setDragImage(header, 20, 20);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
container.addEventListener("dragover", (event) => {
|
|
170
|
+
if (!draggedProjectId) return;
|
|
171
|
+
const target = event.target.closest(selector);
|
|
172
|
+
if (!target || projectId(target) === draggedProjectId) return;
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
|
175
|
+
container.querySelectorAll(`${selector}[data-project-drop-edge]`).forEach(group => group.removeAttribute("data-project-drop-edge"));
|
|
176
|
+
const bounds = target.getBoundingClientRect();
|
|
177
|
+
target.dataset.projectDropEdge = event.clientY > bounds.top + bounds.height / 2 ? "bottom" : "top";
|
|
178
|
+
});
|
|
179
|
+
container.addEventListener("drop", (event) => {
|
|
180
|
+
if (!draggedProjectId) return;
|
|
181
|
+
const target = event.target.closest(selector);
|
|
182
|
+
if (!target || projectId(target) === draggedProjectId) return;
|
|
183
|
+
event.preventDefault();
|
|
184
|
+
event.stopPropagation();
|
|
185
|
+
const bounds = target.getBoundingClientRect();
|
|
186
|
+
const changed = commitProjectPosition(
|
|
187
|
+
draggedProjectId,
|
|
188
|
+
projectId(target),
|
|
189
|
+
event.clientY > bounds.top + bounds.height / 2,
|
|
190
|
+
);
|
|
191
|
+
finishProjectDrag();
|
|
192
|
+
if (!changed) clearProjectDropState();
|
|
193
|
+
});
|
|
194
|
+
container.addEventListener("dragend", finishProjectDrag);
|
|
195
|
+
container.addEventListener("dragleave", (event) => {
|
|
196
|
+
if (!container.contains(event.relatedTarget)) clearProjectDropState();
|
|
197
|
+
});
|
|
198
|
+
container.addEventListener("keydown", (event) => {
|
|
199
|
+
const header = event.target.closest(".control-project-header[draggable='true']");
|
|
200
|
+
if (!header || event.target !== header || !event.altKey || !["ArrowUp", "ArrowDown"].includes(event.key)) return;
|
|
201
|
+
const group = header.closest(selector);
|
|
202
|
+
const groups = Array.from(container.querySelectorAll(selector));
|
|
203
|
+
const current = groups.indexOf(group);
|
|
204
|
+
const offset = event.key === "ArrowUp" ? -1 : 1;
|
|
205
|
+
const target = groups[current + offset];
|
|
206
|
+
if (current < 0 || !target) return;
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
event.stopPropagation();
|
|
209
|
+
commitProjectPosition(projectId(group), projectId(target), offset > 0, true);
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
121
213
|
const managementFilterLabel = value => value === "all" ? window.LoadToAgentI18n.t("management.filter_all") : window.LoadToAgentI18n.t(`management.health.${value}`);
|
|
122
214
|
const announceManagementFilter = value => announce(window.LoadToAgentI18n.t("management.filter_results", {
|
|
123
215
|
filter: managementFilterLabel(value),
|
|
@@ -336,7 +428,13 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
336
428
|
|
|
337
429
|
function bindLiveAgentEvents() {
|
|
338
430
|
bindSortableSessionList($("#liveSessionGrid"), "[data-control-session][data-session-sortable]");
|
|
431
|
+
bindSortableProjectGroups($("#liveSessionGrid"));
|
|
339
432
|
$("#liveSessionGrid").addEventListener("click", async (event) => {
|
|
433
|
+
if (Date.now() - projectDragEndedAt < 250 && event.target.closest(".control-project-header")) {
|
|
434
|
+
event.preventDefault();
|
|
435
|
+
event.stopPropagation();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
340
438
|
if (sessionDragJustEnded) return;
|
|
341
439
|
const archive = event.target.closest("[data-session-archive]");
|
|
342
440
|
if (archive) {
|
|
@@ -11,6 +11,7 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
|
|
|
11
11
|
compact,
|
|
12
12
|
isLiveSession,
|
|
13
13
|
isControlRoomSession = isLiveSession,
|
|
14
|
+
isSessionManuallyArchived = () => false,
|
|
14
15
|
stableSessionSort = sessions => [...sessions],
|
|
15
16
|
} = context;
|
|
16
17
|
|
|
@@ -33,7 +34,7 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
|
|
|
33
34
|
let current = session;
|
|
34
35
|
const seen = new Set();
|
|
35
36
|
while (current && !seen.has(current.id)) {
|
|
36
|
-
included.add(current.id);
|
|
37
|
+
if (!isSessionManuallyArchived(current) || isControlRoomSession(current)) included.add(current.id);
|
|
37
38
|
seen.add(current.id);
|
|
38
39
|
current = current.parentId ? byId.get(current.parentId) : null;
|
|
39
40
|
}
|
|
@@ -58,7 +59,10 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
|
|
|
58
59
|
}
|
|
59
60
|
for (const id of [...included]) {
|
|
60
61
|
const session = byId.get(id);
|
|
61
|
-
for (const childId of (session && session.childIds) || [])
|
|
62
|
+
for (const childId of (session && session.childIds) || []) {
|
|
63
|
+
const child = byId.get(childId);
|
|
64
|
+
if (child && (!isSessionManuallyArchived(child) || isControlRoomSession(child))) included.add(childId);
|
|
65
|
+
}
|
|
62
66
|
}
|
|
63
67
|
return { byId, included, nodes: sessions.filter((session) => included.has(session.id)) };
|
|
64
68
|
}
|