loadtoagent 1.3.3 → 1.3.5
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 +5 -2
- 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/nodePtyRuntime.js +58 -0
- package/src/processMonitor.js +68 -7
- package/src/terminalHost.js +104 -5
- package/src/terminalManager.js +21 -3
|
@@ -3,74 +3,121 @@
|
|
|
3
3
|
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
4
4
|
|
|
5
5
|
window.LoadToAgentAppFactories.createDrawerContent = function createDrawerContent(context = {}) {
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
esc, uiLocale, state, messageContentHtml, compact, fullNumber, timeOnly, providerInfo, statusIcon, agentPathTaskName, snapshotSession,
|
|
8
|
+
controlRoomAgentGoal, inferredExecutionSummary, executionActivityLabel, executionActivityStatus,
|
|
9
|
+
} = context;
|
|
7
10
|
const t = (key, params) => window.LoadToAgentI18n.t(key, params);
|
|
8
11
|
|
|
12
|
+
function conversationTurns(session, options = {}) {
|
|
13
|
+
const turns = [];
|
|
14
|
+
let current = null;
|
|
15
|
+
for (const message of session.messages || []) {
|
|
16
|
+
if (!message) continue;
|
|
17
|
+
if (message.role === "user") {
|
|
18
|
+
current = { id: message.id || `turn-${turns.length}`, user: message, assistants: [], activityAfterAssistant: false };
|
|
19
|
+
turns.push(current);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (message.role === "assistant") {
|
|
23
|
+
if (!current) {
|
|
24
|
+
const title = String(session.title || "").trim();
|
|
25
|
+
const syntheticUser = options.synthesizeRequest === false || !title
|
|
26
|
+
? null
|
|
27
|
+
: {
|
|
28
|
+
id: `${session.id}:request`, role: "user", text: title,
|
|
29
|
+
timestamp: session.startedAt || message.timestamp,
|
|
30
|
+
};
|
|
31
|
+
current = { id: syntheticUser?.id || message.id || `turn-${turns.length}`, user: syntheticUser, assistants: [], activityAfterAssistant: false };
|
|
32
|
+
turns.push(current);
|
|
33
|
+
}
|
|
34
|
+
current.assistants.push(message);
|
|
35
|
+
current.activityAfterAssistant = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (message.role === "tool" && current && current.assistants.length) current.activityAfterAssistant = true;
|
|
39
|
+
}
|
|
40
|
+
const latestIndex = turns.length - 1;
|
|
41
|
+
const live = session.status === "running" || session.status === "starting";
|
|
42
|
+
return turns.map((turn, index) => ({
|
|
43
|
+
...turn,
|
|
44
|
+
representative: turn.assistants.at(-1) || null,
|
|
45
|
+
progress: turn.assistants.slice(0, -1),
|
|
46
|
+
live: live && index === latestIndex,
|
|
47
|
+
awaitingFinal: Boolean(turn.activityAfterAssistant),
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function conversationRowHtml(message, session, options = {}) {
|
|
52
|
+
if (!message) return "";
|
|
53
|
+
const assistant = message.role === "assistant";
|
|
54
|
+
const label = assistant ? options.assistantLabel : options.userLabel;
|
|
55
|
+
const avatar = assistant ? providerInfo(session.provider).mark : "ME";
|
|
56
|
+
const fullTime = new Date(message.timestamp).toLocaleString(uiLocale());
|
|
57
|
+
const answerKind = assistant && options.answerKind
|
|
58
|
+
? `<span class="chat-answer-kind${options.live ? " is-live" : ""}">${esc(options.answerKind)}</span>`
|
|
59
|
+
: "";
|
|
60
|
+
return `<div class="chat-row ${assistant ? "assistant" : "user"}" data-message-id="${esc(message.id || "")}">
|
|
61
|
+
<span class="chat-avatar">${esc(avatar)}</span>
|
|
62
|
+
<div class="chat-bubble">
|
|
63
|
+
<div class="chat-bubble-head">
|
|
64
|
+
<b>${esc(label)}</b>
|
|
65
|
+
<span title="${esc(fullTime)}">${esc(timeOnly(message.timestamp))}</span>${answerKind}
|
|
66
|
+
</div>${messageContentHtml(message, session.id)}</div>
|
|
67
|
+
</div>`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function progressUpdatesHtml(turn, session) {
|
|
71
|
+
if (!turn.progress.length) return "";
|
|
72
|
+
const rows = turn.progress.map((message, index) => {
|
|
73
|
+
const fullTime = new Date(message.timestamp).toLocaleString(uiLocale());
|
|
74
|
+
return `<article data-progress-message-id="${esc(message.id || "")}">
|
|
75
|
+
<header><b>${esc(t("drawer.progress_update_item", { count: index + 1 }))}</b><time title="${esc(fullTime)}">${esc(timeOnly(message.timestamp))}</time></header>
|
|
76
|
+
${messageContentHtml(message, session.id)}
|
|
77
|
+
</article>`;
|
|
78
|
+
}).join("");
|
|
79
|
+
return `<details class="chat-progress-updates" data-progress-count="${turn.progress.length}"
|
|
80
|
+
data-disclosure-key="${esc(`drawer:${session.id}:turn:${turn.id}:progress`)}">
|
|
81
|
+
<summary><span><b>${esc(t("drawer.progress_updates", { count: turn.progress.length }))}</b>
|
|
82
|
+
<small>${esc(t("drawer.progress_updates_help"))}</small></span><i aria-hidden="true">↓</i></summary>
|
|
83
|
+
<div class="chat-progress-list">${rows}</div>
|
|
84
|
+
</details>`;
|
|
85
|
+
}
|
|
86
|
+
|
|
9
87
|
function chatHtml(session, options = {}) {
|
|
10
88
|
const messages = session.messages || [];
|
|
11
89
|
if (!messages.length) return `<div class="empty-state"><h3>${esc(t("drawer.no_conversation"))}</h3></div>`;
|
|
12
90
|
const userLabel = options.userLabel || t("drawer.user");
|
|
13
91
|
const assistantLabel = options.assistantLabel || providerInfo(session.provider).label;
|
|
14
92
|
const conversationLabel = options.conversationLabel || t("drawer.conversation");
|
|
15
|
-
const
|
|
16
|
-
const
|
|
93
|
+
const turns = conversationTurns(session, options);
|
|
94
|
+
const progressCount = turns.reduce((sum, turn) => sum + turn.progress.length, 0);
|
|
17
95
|
const omitted = Number(session.omittedMessages || 0);
|
|
18
96
|
const notice =
|
|
19
97
|
omitted || session.truncated
|
|
20
98
|
? `<div class="chat-truncated">${esc(t("drawer.recent_history"))}${omitted ? ` · ${esc(t("drawer.messages_omitted", { count: omitted.toLocaleString(uiLocale()) }))}` : ""}</div>`
|
|
21
99
|
: "";
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
: message.role === "system"
|
|
39
|
-
? t("drawer.system")
|
|
40
|
-
: userLabel;
|
|
41
|
-
const avatar = role === "assistant" ? providerInfo(session.provider).mark : role === "tool" ? "⌘" : role === "system" ? "i" : "ME";
|
|
42
|
-
const fullTime = new Date(message.timestamp).toLocaleString(uiLocale());
|
|
43
|
-
return `<div class="chat-row ${role}" data-message-id="${esc(message.id || "")}">
|
|
44
|
-
<span class="chat-avatar">${esc(avatar)}</span>
|
|
45
|
-
<div class="chat-bubble">
|
|
46
|
-
<div class="chat-bubble-head">
|
|
47
|
-
<b>${esc(label)}</b>
|
|
48
|
-
<span title="${esc(fullTime)}">${esc(timeOnly(message.timestamp))}</span>
|
|
49
|
-
${message.status ? `<span>${esc(statusLabel(message.status))}</span>` : ""}
|
|
50
|
-
</div>${messageContentHtml(renderedMessage, session.id)}</div>
|
|
51
|
-
</div>`;
|
|
52
|
-
})
|
|
53
|
-
.join("");
|
|
54
|
-
const activityHtml = activities.length
|
|
55
|
-
? `<details class="chat-activities" data-disclosure-key="${esc(`drawer:${session.id}:activities`)}">
|
|
56
|
-
<summary>${esc(t("drawer.activities_view", { count: activities.length }))}</summary>
|
|
57
|
-
<div>${activities
|
|
58
|
-
.map(
|
|
59
|
-
(message) => `<article>
|
|
60
|
-
<header>
|
|
61
|
-
<b>${esc(window.LoadToAgentI18n.observedText(message.title || (message.role === "tool" ? t("drawer.tool_execution") : t("drawer.system"))))}</b>
|
|
62
|
-
<span>${esc(statusLabel(message.status))} · ${esc(timeOnly(message.timestamp))}</span>
|
|
63
|
-
</header>${messageContentHtml({ ...message, text: window.LoadToAgentI18n.observedText(message.text) }, session.id)}</article>`,
|
|
64
|
-
)
|
|
65
|
-
.join("")}</div>
|
|
66
|
-
</details>`
|
|
67
|
-
: "";
|
|
68
|
-
const emptyConversation = conversation.length ? "" : `<div class="empty-state compact"><h3>${esc(t("drawer.no_user_ai_conversation"))}</h3></div>`;
|
|
100
|
+
const rows = turns.map((turn) => {
|
|
101
|
+
const user = conversationRowHtml(turn.user, session, { userLabel, assistantLabel });
|
|
102
|
+
const representative = conversationRowHtml(turn.representative, session, {
|
|
103
|
+
userLabel,
|
|
104
|
+
assistantLabel,
|
|
105
|
+
answerKind: t(turn.live ? "drawer.current_progress" : turn.awaitingFinal ? "drawer.last_progress" : "drawer.final_answer"),
|
|
106
|
+
live: turn.live,
|
|
107
|
+
});
|
|
108
|
+
const waiting = turn.live && !turn.representative
|
|
109
|
+
? `<div class="chat-turn-waiting"><span aria-hidden="true"></span><b>${esc(t("drawer.preparing_response"))}</b></div>`
|
|
110
|
+
: "";
|
|
111
|
+
return `<section class="chat-turn${turn.live ? " is-live" : ""}" data-conversation-turn="${esc(turn.id)}">
|
|
112
|
+
${user}${representative}${waiting}${progressUpdatesHtml(turn, session)}
|
|
113
|
+
</section>`;
|
|
114
|
+
}).join("");
|
|
115
|
+
const emptyConversation = turns.length ? "" : `<div class="empty-state compact"><h3>${esc(t("drawer.no_user_ai_conversation"))}</h3></div>`;
|
|
69
116
|
return `${notice}<div class="chat-history-head">
|
|
70
|
-
<span>${esc(t("drawer.
|
|
117
|
+
<span>${esc(t("drawer.turn_summary", { label: conversationLabel, count: turns.length, updates: progressCount ? ` · ${t("drawer.progress_updates", { count: progressCount })}` : "" }))}</span>
|
|
71
118
|
<button type="button" data-scroll-latest>${esc(t("drawer.latest_conversation"))} ↓</button>
|
|
72
119
|
</div>
|
|
73
|
-
<div class="chat-list">${rows}${emptyConversation}
|
|
120
|
+
<div class="chat-list">${rows}${emptyConversation}<div class="chat-latest-anchor" aria-label="${esc(t("drawer.latest_conversation"))}">
|
|
74
121
|
</div>
|
|
75
122
|
</div>`;
|
|
76
123
|
}
|
|
@@ -153,12 +200,26 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
153
200
|
}
|
|
154
201
|
|
|
155
202
|
function subagentWorkMessages(session) {
|
|
156
|
-
const
|
|
203
|
+
const parent = session.parentId ? state.details.get(session.parentId) || snapshotSession(session.parentId) : null;
|
|
204
|
+
const delegation = session.delegation || {};
|
|
205
|
+
const startValue = delegation.startedAt || session.startedAt || "";
|
|
206
|
+
const startedAt = startValue ? Date.parse(startValue) : Number.NaN;
|
|
207
|
+
const normalizedText = value => String(value || "").replace(/\s+/g, " ").trim();
|
|
208
|
+
const messageKey = message => `${message.role || ""}:${normalizedText(message.text)}`;
|
|
209
|
+
const parentMessageIds = new Set((parent?.messages || []).map(message => String(message.id || "")).filter(Boolean));
|
|
210
|
+
const parentMessageKeys = new Set((parent?.messages || []).map(messageKey));
|
|
211
|
+
const messages = (session.messages || []).filter(message => {
|
|
212
|
+
if (!message || !["user", "assistant"].includes(message.role) || message.protected || !normalizedText(message.text)) return false;
|
|
213
|
+
const messageAt = message.timestamp ? Date.parse(message.timestamp) : Number.NaN;
|
|
214
|
+
if (Number.isFinite(startedAt) && startedAt > 0 && Number.isFinite(messageAt) && messageAt < startedAt - 2000) return false;
|
|
215
|
+
const inheritedId = message.id && parentMessageIds.has(String(message.id));
|
|
216
|
+
const inheritedText = parentMessageKeys.has(messageKey(message));
|
|
217
|
+
return !inheritedId && !inheritedText;
|
|
218
|
+
});
|
|
157
219
|
const hasConversation = messages.some((message) =>
|
|
158
220
|
(message.role === "user" || message.role === "assistant") && String(message.text || "").trim(),
|
|
159
221
|
);
|
|
160
222
|
if (hasConversation) return messages;
|
|
161
|
-
const delegation = session.delegation || {};
|
|
162
223
|
if (delegation.assignmentObserved && !delegation.assignmentProtected && String(delegation.assignment || "").trim()) {
|
|
163
224
|
messages.push({
|
|
164
225
|
id: `${session.id}:delegation`, role: "user", text: delegation.assignment,
|
|
@@ -201,20 +262,77 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
201
262
|
}
|
|
202
263
|
|
|
203
264
|
function subagentConversationHtml(session) {
|
|
204
|
-
const
|
|
265
|
+
const delegation = session.delegation || {};
|
|
266
|
+
const parent = session.parentId ? state.details.get(session.parentId) || snapshotSession(session.parentId) : null;
|
|
267
|
+
const assignmentEvent = subagentCoordinationEvents(session).find(event => event.kind === "assignment" && String(event.text || "").trim());
|
|
268
|
+
const assignment = String(delegation.assignment || assignmentEvent?.text || delegation.taskName || session.taskName || session.title || "").trim();
|
|
269
|
+
let assignmentRemoved = false;
|
|
270
|
+
const messages = subagentWorkMessages(session).filter(message => {
|
|
271
|
+
if (assignmentRemoved || message.role !== "user" || !assignment) return true;
|
|
272
|
+
if (String(message.text || "").replace(/\s+/g, " ").trim() !== assignment.replace(/\s+/g, " ").trim()) return true;
|
|
273
|
+
assignmentRemoved = true;
|
|
274
|
+
return false;
|
|
275
|
+
});
|
|
205
276
|
const conversationCount = messages.filter((message) => message.role === "user" || message.role === "assistant").length;
|
|
206
277
|
const workSession = { ...session, messages };
|
|
207
278
|
const sourceCopy = session.source === "collaboration-history"
|
|
208
279
|
? t("drawer.subagent_history_reconstructed")
|
|
209
280
|
: t("drawer.subagent_history_actual");
|
|
210
|
-
return `<section class="subagent-
|
|
211
|
-
<b>${esc(t("
|
|
281
|
+
return `<section class="subagent-assignment-card" data-subagent-assignment="true">
|
|
282
|
+
<span aria-hidden="true">⌁</span><div><b>${esc(t("control.main_assignment"))}</b>${parent ? `<small>${esc(t("control.created_from"))} · ${esc(parent.title)}</small>` : ""}<p>${esc(assignment || t("management.signal_unavailable"))}</p></div>
|
|
283
|
+
</section><section class="subagent-work-source" data-subagent-work-messages="${conversationCount}" data-conversation-scope="subagent-only">
|
|
284
|
+
<b>${esc(t("control.subagent_conversation"))}</b><span>${esc(sourceCopy)}</span>
|
|
212
285
|
</section>${chatHtml(workSession, {
|
|
213
|
-
userLabel: t("drawer.
|
|
286
|
+
userLabel: t("drawer.user"),
|
|
214
287
|
assistantLabel: session.agentName || t("drawer.sub_ai"),
|
|
215
288
|
conversationLabel: t("drawer.work_history"),
|
|
289
|
+
synthesizeRequest: false,
|
|
216
290
|
})}${subagentCoordinationHtml(session)}`;
|
|
217
291
|
}
|
|
218
292
|
|
|
219
|
-
|
|
293
|
+
function executionActivityDetailHtml(session, activity) {
|
|
294
|
+
if (!activity) return `<div class="empty-state"><h3>${esc(t("drawer.execution_unavailable"))}</h3></div>`;
|
|
295
|
+
const purpose = inferredExecutionSummary(activity);
|
|
296
|
+
const ownerGoal = controlRoomAgentGoal(session, 180);
|
|
297
|
+
const runtime = activity.runtime || activity.tool || t("graph.runtime_unknown");
|
|
298
|
+
const handle = activity.backgroundId
|
|
299
|
+
? `${activity.backgroundIdType || t("graph.execution_handle")} · ${activity.backgroundId}`
|
|
300
|
+
: "";
|
|
301
|
+
const command = String(activity.command || activity.label || purpose.full || "").trim();
|
|
302
|
+
const output = String(activity.output || "").trim();
|
|
303
|
+
const status = executionActivityStatus(activity);
|
|
304
|
+
const ownerLabel = session.parentId
|
|
305
|
+
? `${t("control.subagent")} · ${session.agentName || session.taskName || providerInfo(session.provider).label}`
|
|
306
|
+
: `${t("control.main_agent")} · ${session.agentName || providerInfo(session.provider).label}`;
|
|
307
|
+
const timeline = [
|
|
308
|
+
activity.startedAt ? { label: t("drawer.execution_started"), value: activity.startedAt } : null,
|
|
309
|
+
activity.updatedAt ? { label: activity.status === "running" ? t("drawer.execution_latest_activity") : t("drawer.execution_finished"), value: activity.updatedAt } : null,
|
|
310
|
+
].filter(Boolean);
|
|
311
|
+
return `<div class="execution-drawer" data-execution-detail="${esc(activity.id)}" data-conversation-scope="execution-only">
|
|
312
|
+
<section class="execution-purpose-card">
|
|
313
|
+
<span class="execution-purpose-icon" aria-hidden="true">${activity.kind === "shell" ? "›_" : "◌"}</span>
|
|
314
|
+
<div><small>${esc(t("drawer.execution_purpose"))}</small><b>${esc(purpose.text)}</b><p>${esc(t("drawer.execution_owner_context", { owner: ownerLabel, task: ownerGoal.text }))}</p></div>
|
|
315
|
+
</section>
|
|
316
|
+
<section class="execution-process-card">
|
|
317
|
+
<header><span><small>${esc(executionActivityLabel(activity))}</small><b>${esc(status)}</b></span><em class="${activity.status === "running" ? "is-running" : ""}"><i aria-hidden="true"></i>${esc(activity.statusDetail || status)}</em></header>
|
|
318
|
+
<dl>
|
|
319
|
+
<div><dt>${esc(t("graph.execution_runtime"))}</dt><dd>${esc(runtime)}</dd></div>
|
|
320
|
+
${activity.cwd ? `<div><dt>${esc(t("graph.execution_workdir"))}</dt><dd title="${esc(activity.cwd)}">${esc(activity.cwd)}</dd></div>` : ""}
|
|
321
|
+
${handle ? `<div><dt>${esc(t("graph.execution_handle"))}</dt><dd>${esc(handle)}</dd></div>` : ""}
|
|
322
|
+
${activity.exitCode != null ? `<div><dt>${esc(t("drawer.execution_exit_code"))}</dt><dd>${esc(activity.exitCode)}</dd></div>` : ""}
|
|
323
|
+
</dl>
|
|
324
|
+
</section>
|
|
325
|
+
<section class="execution-code-card">
|
|
326
|
+
<header><span><small>${esc(t("drawer.execution_command_from", { owner: ownerLabel }))}</small><b>${esc(t("graph.execution_command"))}</b></span>${command ? `<button type="button" data-copy-text="${esc(command)}">${esc(t("graph.copy_command"))}</button>` : ""}</header>
|
|
327
|
+
${command ? `<pre><code>${esc(command)}</code></pre>` : `<p>${esc(t("drawer.execution_command_unavailable"))}</p>`}
|
|
328
|
+
</section>
|
|
329
|
+
<section class="execution-code-card output-card">
|
|
330
|
+
<header><span><small>${esc(t("drawer.execution_output_help"))}</small><b>${esc(t("graph.execution_output"))}</b></span>${output ? `<button type="button" data-copy-text="${esc(output)}">${esc(t("graph.copy_output"))}</button>` : ""}</header>
|
|
331
|
+
${output ? `<pre>${esc(output)}</pre>` : `<p>${esc(activity.status === "running" ? t("drawer.execution_waiting_output") : t("graph.execution_output_unavailable"))}</p>`}
|
|
332
|
+
</section>
|
|
333
|
+
${timeline.length ? `<section class="execution-timeline" aria-label="${esc(t("drawer.execution_timeline"))}">${timeline.map((item, index) => `<div><i aria-hidden="true"></i><span><b>${esc(item.label)}</b><time title="${esc(item.value)}">${esc(new Date(item.value).toLocaleString(uiLocale()))}</time></span>${index === timeline.length - 1 && activity.status === "running" ? `<em>${esc(t("graph.execution_running"))}</em>` : ""}</div>`).join("")}</section>` : ""}
|
|
334
|
+
</div>`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { conversationTurns, chatHtml, lifecycleHtml, tokensHtml, subagentCommunicationEvents, subagentCoordinationEvents, subagentTextPreview, subagentConversationHtml, executionActivityDetailHtml };
|
|
220
338
|
};
|
|
@@ -10,7 +10,11 @@ window.LoadToAgentAppFactories.createDrawerData = function createDrawerData(cont
|
|
|
10
10
|
|
|
11
11
|
async function loadSessionDetail(id, force = false) {
|
|
12
12
|
if (!force && state.details.has(id)) return state.details.get(id);
|
|
13
|
-
|
|
13
|
+
// A live snapshot can advance again while the previous detail request is
|
|
14
|
+
// still running. Share that request instead of stacking more full-history
|
|
15
|
+
// reads for the same session.
|
|
16
|
+
if (detailRequests.has(id)) return detailRequests.get(id).promise;
|
|
17
|
+
const hadCachedDetail = state.details.has(id);
|
|
14
18
|
const generation = ++detailRequestGeneration;
|
|
15
19
|
state.detailErrors.delete(id);
|
|
16
20
|
state.detailLoadingIds.add(id);
|
|
@@ -29,7 +33,7 @@ window.LoadToAgentAppFactories.createDrawerData = function createDrawerData(cont
|
|
|
29
33
|
detailRequests.delete(id);
|
|
30
34
|
state.detailLoadingIds.delete(id);
|
|
31
35
|
if (state.selectedId === id) {
|
|
32
|
-
state.drawerForceLatest = state.drawerTab === "chat";
|
|
36
|
+
if (!hadCachedDetail) state.drawerForceLatest = state.drawerTab === "chat";
|
|
33
37
|
context.renderDrawer();
|
|
34
38
|
}
|
|
35
39
|
}
|
|
@@ -44,7 +48,7 @@ window.LoadToAgentAppFactories.createDrawerData = function createDrawerData(cont
|
|
|
44
48
|
try {
|
|
45
49
|
const detail = await window.loadtoagent.sessionDetail(child.parentId);
|
|
46
50
|
if (detail) state.details.set(child.parentId, detail);
|
|
47
|
-
if (state.drawerMode === "subagent" && state.selectedId === child.id) context.renderDrawer();
|
|
51
|
+
if ((state.drawerMode === "subagent" || state.drawerMode === "execution") && state.selectedId === child.id) context.renderDrawer();
|
|
48
52
|
} catch (error) {
|
|
49
53
|
reportRecoverableError("subagent-parent-detail", error);
|
|
50
54
|
}
|
package/renderer/app-drawer.js
CHANGED
|
@@ -8,7 +8,8 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
8
8
|
$, $$, esc, state, motionPreference, motionState, STATUS, markGuideStep, rememberDialogTrigger, restoreDialogTrigger, setDialogOpenState,
|
|
9
9
|
providerInfo, isLiveSession, subagentWorkState, subagentWorkLabel, isProjectlessSession, sessionOriginPath, sessionWorkspaceLabel,
|
|
10
10
|
agentResumeSupport, originAppInfo, selectedSession, snapshotSession, loadSessionDetail, loadSubagentParentDetail,
|
|
11
|
-
chatHtml, lifecycleHtml, tokensHtml, outcomeHtml, subagentCoordinationEvents, subagentConversationHtml,
|
|
11
|
+
chatHtml, lifecycleHtml, tokensHtml, outcomeHtml, subagentCoordinationEvents, subagentConversationHtml, executionActivityDetailHtml,
|
|
12
|
+
agentCommandComposer,
|
|
12
13
|
rememberDisclosureStates = () => {}, restoreDisclosureStates = () => {},
|
|
13
14
|
} = context;
|
|
14
15
|
|
|
@@ -17,7 +18,8 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
17
18
|
markGuideStep("detail");
|
|
18
19
|
state.selectedId = id;
|
|
19
20
|
state.drawerMode = "session";
|
|
20
|
-
state.
|
|
21
|
+
state.drawerExecutionId = null;
|
|
22
|
+
state.drawerTab = "chat";
|
|
21
23
|
state.drawerForceLatest = true;
|
|
22
24
|
clearTimeout(motionState.drawerTimer);
|
|
23
25
|
$("#drawerBackdrop").classList.remove("hidden");
|
|
@@ -36,7 +38,9 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
36
38
|
markGuideStep("detail");
|
|
37
39
|
state.selectedId = id;
|
|
38
40
|
state.drawerMode = "subagent";
|
|
41
|
+
state.drawerExecutionId = null;
|
|
39
42
|
state.drawerTab = "chat";
|
|
43
|
+
state.agentCommandRoutes.delete(id);
|
|
40
44
|
state.drawerForceLatest = true;
|
|
41
45
|
clearTimeout(motionState.drawerTimer);
|
|
42
46
|
$("#drawerBackdrop").classList.remove("hidden");
|
|
@@ -49,6 +53,27 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
49
53
|
setTimeout(() => $("#closeDrawerBtn").focus({ preventScroll: true }), 0);
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
function openExecutionActivity(ownerId, executionId) {
|
|
57
|
+
const owner = snapshotSession(ownerId) || state.details.get(ownerId);
|
|
58
|
+
if (!owner) return;
|
|
59
|
+
rememberDialogTrigger();
|
|
60
|
+
markGuideStep("detail");
|
|
61
|
+
state.selectedId = ownerId;
|
|
62
|
+
state.drawerMode = "execution";
|
|
63
|
+
state.drawerExecutionId = executionId;
|
|
64
|
+
state.drawerTab = "chat";
|
|
65
|
+
state.drawerForceLatest = false;
|
|
66
|
+
clearTimeout(motionState.drawerTimer);
|
|
67
|
+
$("#drawerBackdrop").classList.remove("hidden");
|
|
68
|
+
$("#drawerBackdrop").classList.remove("closing");
|
|
69
|
+
$("#detailDrawer").classList.add("open");
|
|
70
|
+
setDialogOpenState($("#detailDrawer"), true);
|
|
71
|
+
renderDrawer();
|
|
72
|
+
loadSessionDetail(ownerId);
|
|
73
|
+
if (owner.parentId) loadSubagentParentDetail(owner);
|
|
74
|
+
setTimeout(() => $("#closeDrawerBtn").focus({ preventScroll: true }), 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
52
77
|
function closeDrawer(restoreFocus = true) {
|
|
53
78
|
if (!$("#detailDrawer").classList.contains("open")) return;
|
|
54
79
|
const drawerGeneration = motionState.dialogGeneration;
|
|
@@ -73,14 +98,28 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
73
98
|
if (!session) return closeDrawer();
|
|
74
99
|
const provider = providerInfo(session.provider);
|
|
75
100
|
const subagentMode = state.drawerMode === "subagent" && Boolean(session.parentId);
|
|
76
|
-
const
|
|
101
|
+
const executionMode = state.drawerMode === "execution" && Boolean(state.drawerExecutionId);
|
|
102
|
+
const snapshot = snapshotSession(session.id);
|
|
103
|
+
const activity = executionMode
|
|
104
|
+
? (session.executions || []).find(item => item.id === state.drawerExecutionId)
|
|
105
|
+
|| (snapshot?.executions || []).find(item => item.id === state.drawerExecutionId)
|
|
106
|
+
|| null
|
|
107
|
+
: null;
|
|
108
|
+
// Live sessions refresh frequently. Keep an already loaded conversation on
|
|
109
|
+
// screen while its newer detail is fetched instead of flashing the loader.
|
|
110
|
+
const detailLoading = state.detailLoadingIds.has(state.selectedId) && !state.details.has(state.selectedId);
|
|
111
|
+
$("#detailDrawer").dataset.mode = executionMode ? "execution" : subagentMode ? "subagent" : "session";
|
|
77
112
|
$("#detailDrawer").style.setProperty("--drawer-provider", provider.accent);
|
|
78
113
|
$("#drawerProviderMark").style.setProperty("--provider", provider.accent);
|
|
79
|
-
$("#drawerProviderMark").textContent = provider.mark;
|
|
80
|
-
$("#drawerProvider").textContent =
|
|
81
|
-
? t("drawer.
|
|
114
|
+
$("#drawerProviderMark").textContent = executionMode && activity?.kind === "shell" ? ">_" : provider.mark;
|
|
115
|
+
$("#drawerProvider").textContent = executionMode
|
|
116
|
+
? `${activity?.runtime || activity?.tool || t("drawer.execution_unit")} · ${activity ? context.executionActivityStatus(activity) : t("drawer.unknown")}`
|
|
117
|
+
: subagentMode
|
|
118
|
+
? `${t("control.subagent")} · ${STATUS[session.status] || session.status}`
|
|
82
119
|
: `${provider.company} · ${STATUS[session.status] || session.status}`;
|
|
83
|
-
const drawerTitle =
|
|
120
|
+
const drawerTitle = executionMode
|
|
121
|
+
? context.inferredExecutionSummary(activity || {}).text
|
|
122
|
+
: subagentMode ? session.title || session.taskName || (session.delegation && session.delegation.taskName) : session.title;
|
|
84
123
|
$("#drawerTitle").textContent = drawerTitle;
|
|
85
124
|
$("#drawerTitle").title = drawerTitle;
|
|
86
125
|
const stopping = session.runId && state.stopRequests.has(session.runId);
|
|
@@ -97,32 +136,23 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
97
136
|
<b>${esc(t(originAppInfo(session) ? "drawer.continue_background_terminal" : "drawer.resume_in_terminal"))}</b>
|
|
98
137
|
</button>`
|
|
99
138
|
: "";
|
|
100
|
-
const communicationCount = subagentMode ? subagentCoordinationEvents(session).length : 0;
|
|
101
|
-
const subagentMessageCount = subagentMode
|
|
102
|
-
? (session.messages || []).filter((message) => message.role === "user" || message.role === "assistant").length
|
|
103
|
-
: 0;
|
|
104
|
-
const taskId = String(session.externalId || session.id || "");
|
|
105
|
-
const copyTask = taskId
|
|
106
|
-
? `<button type="button" class="meta-chip meta-copy" data-copy-text="${esc(taskId)}" aria-label="${esc(t("quality.copy_task_id"))}">${esc(t("quality.task_id"))} <b>${esc(taskId.slice(0, 12))}</b><span aria-hidden="true">⧉</span></button>`
|
|
107
|
-
: "";
|
|
108
139
|
const originPath = sessionOriginPath(session);
|
|
109
140
|
const copyWorkspace = !isProjectlessSession(session) && originPath
|
|
110
141
|
? `<button type="button" class="meta-chip meta-copy origin-project-meta" data-copy-text="${esc(originPath)}" aria-label="${esc(t("quality.copy_workspace"))}">${esc(t("project.origin"))} <b>${esc(sessionWorkspaceLabel(session))}</b><span aria-hidden="true">⧉</span></button>`
|
|
111
142
|
: "";
|
|
112
|
-
$("#drawerMeta").innerHTML =
|
|
143
|
+
$("#drawerMeta").innerHTML = executionMode
|
|
144
|
+
? `<span class="meta-chip work-state ${activity?.status === "running" ? "working" : "resting"}"><b>${esc(activity ? context.executionActivityStatus(activity) : t("drawer.unknown"))}</b></span>
|
|
145
|
+
<span class="meta-chip">${esc(session.parentId ? t("control.subagent") : t("control.main_agent"))} <b>${esc(session.agentName || provider.label)}</b></span>
|
|
146
|
+
${activity?.backgroundId ? `<span class="meta-chip">${esc(t("graph.execution_handle"))} <b>${esc(activity.backgroundId)}</b></span>` : ""}`
|
|
147
|
+
: subagentMode
|
|
113
148
|
? `<span class="meta-chip work-state ${subagentWorkState(session)}">
|
|
114
149
|
<b>${esc(subagentWorkLabel(session))}</b>
|
|
115
150
|
</span>
|
|
116
|
-
<span class="meta-chip">${esc(t("drawer.model"))} <b>${esc(session.model || t("drawer.unknown"))}</b
|
|
117
|
-
</span>
|
|
118
|
-
<span class="meta-chip">${esc(t("drawer.work_history"))} <b>${esc(t("drawer.event_count", { count: subagentMessageCount }))}</b>
|
|
119
|
-
</span>
|
|
120
|
-
<span class="meta-chip">${esc(t("drawer.main_instructions_results"))} <b>${esc(t("drawer.event_count", { count: communicationCount }))}</b>
|
|
121
|
-
</span>${copyTask}${copyWorkspace}${resume}`
|
|
151
|
+
<span class="meta-chip">${esc(t("drawer.model"))} <b>${esc(session.model || t("drawer.unknown"))}</b></span>${resume}`
|
|
122
152
|
: `<span class="meta-chip">${esc(t("drawer.model"))} <b>${esc(session.model || t("drawer.unknown"))}</b>
|
|
123
153
|
</span>
|
|
124
154
|
${copyWorkspace || `<span class="meta-chip origin-project-meta">${esc(t("project.origin"))} <b>${esc(sessionWorkspaceLabel(session))}</b></span>`}
|
|
125
|
-
${
|
|
155
|
+
${
|
|
126
156
|
session.parentId
|
|
127
157
|
? `<span class="meta-chip">⑂ <b>${esc(t("drawer.helper_ai"))}</b>
|
|
128
158
|
</span>`
|
|
@@ -134,9 +164,9 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
134
164
|
: ""
|
|
135
165
|
}${resume}${stop}`;
|
|
136
166
|
$$(".drawer-tab").forEach((tab) => {
|
|
137
|
-
const hidden = subagentMode && tab.dataset.tab !== "chat";
|
|
167
|
+
const hidden = (subagentMode || executionMode) && tab.dataset.tab !== "chat";
|
|
138
168
|
tab.classList.toggle("hidden", hidden);
|
|
139
|
-
if (tab.dataset.tab === "chat") tab.textContent = subagentMode ? t("drawer.work_content") : t("ui.conversation");
|
|
169
|
+
if (tab.dataset.tab === "chat") tab.textContent = executionMode ? t("drawer.execution_process") : subagentMode ? t("drawer.work_content") : t("ui.conversation");
|
|
140
170
|
const active = tab.dataset.tab === state.drawerTab;
|
|
141
171
|
tab.classList.toggle("active", active);
|
|
142
172
|
tab.setAttribute("aria-selected", active ? "true" : "false");
|
|
@@ -163,6 +193,8 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
163
193
|
<span>${esc(detailError)}</span>
|
|
164
194
|
<button type="button" data-retry-detail="${esc(state.selectedId)}">${esc(t("drawer.retry"))}</button>
|
|
165
195
|
</div>`
|
|
196
|
+
: executionMode
|
|
197
|
+
? executionActivityDetailHtml(session, activity)
|
|
166
198
|
: subagentMode
|
|
167
199
|
? subagentConversationHtml(session)
|
|
168
200
|
: state.drawerTab === "summary"
|
|
@@ -172,6 +204,10 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
172
204
|
: state.drawerTab === "lifecycle"
|
|
173
205
|
? lifecycleHtml(session)
|
|
174
206
|
: tokensHtml(session);
|
|
207
|
+
const composer = $("#drawerComposer");
|
|
208
|
+
const showComposer = !executionMode && !detailLoading && !detailError && state.drawerTab === "chat" && typeof agentCommandComposer === "function";
|
|
209
|
+
composer.classList.toggle("hidden", !showComposer);
|
|
210
|
+
composer.innerHTML = showComposer ? agentCommandComposer(session, { conversation: true }) : "";
|
|
175
211
|
restoreDisclosureStates(content);
|
|
176
212
|
content.classList.toggle("motion-content-in", shouldAnimateContent && !motionPreference.matches);
|
|
177
213
|
clearTimeout(motionState.drawerContentTimer);
|
|
@@ -188,13 +224,16 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
188
224
|
if (motionState.drawerScrollGeneration !== scrollGeneration) return;
|
|
189
225
|
const forceLatest = state.drawerForceLatest;
|
|
190
226
|
if (state.drawerTab === "chat" && forceLatest) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
227
|
+
if (subagentMode || executionMode) content.scrollTop = 0;
|
|
228
|
+
else {
|
|
229
|
+
const rows = [...content.querySelectorAll(".chat-row")];
|
|
230
|
+
const latest = rows[rows.length - 1];
|
|
231
|
+
if (latest && latest.offsetHeight > content.clientHeight - 90) {
|
|
232
|
+
const contentTop = content.getBoundingClientRect().top;
|
|
233
|
+
const stickyHeight = content.querySelector(".chat-history-head")?.getBoundingClientRect().height || 0;
|
|
234
|
+
content.scrollTop = Math.max(0, content.scrollTop + latest.getBoundingClientRect().top - contentTop - stickyHeight - 12);
|
|
235
|
+
} else content.scrollTop = content.scrollHeight;
|
|
236
|
+
}
|
|
198
237
|
} else if (state.drawerTab === "chat" && wasAtBottom) content.scrollTop = content.scrollHeight;
|
|
199
238
|
else content.scrollTop = Math.min(previousTop, Math.max(0, content.scrollHeight - content.clientHeight));
|
|
200
239
|
state.drawerForceLatest = false;
|
|
@@ -203,5 +242,5 @@ window.LoadToAgentAppFactories.createDrawer = function createDrawer(context = {}
|
|
|
203
242
|
}
|
|
204
243
|
}
|
|
205
244
|
|
|
206
|
-
return { openDrawer, openSubagentConversation, closeDrawer, renderDrawer };
|
|
245
|
+
return { openDrawer, openSubagentConversation, openExecutionActivity, closeDrawer, renderDrawer };
|
|
207
246
|
};
|
|
@@ -161,6 +161,13 @@ window.LoadToAgentAppFactories.createDialogEventBindings = function createDialog
|
|
|
161
161
|
$(`.drawer-tab[data-tab="${state.drawerTab}"]`)?.focus();
|
|
162
162
|
});
|
|
163
163
|
$("#detailDrawer").addEventListener("click", async (event) => {
|
|
164
|
+
const route = event.target.closest("[data-agent-command-route]");
|
|
165
|
+
if (route) {
|
|
166
|
+
state.agentCommandRoutes.set(route.dataset.agentCommandSession, route.dataset.agentCommandRoute);
|
|
167
|
+
renderDrawer();
|
|
168
|
+
requestAnimationFrame(() => $("#detailDrawer")?.querySelector(`[data-agent-command-session="${CSS.escape(route.dataset.agentCommandSession)}"][data-agent-command-route="${CSS.escape(route.dataset.agentCommandRoute)}"]`)?.focus({ preventScroll: true }));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
164
171
|
const copy = event.target.closest("[data-copy-text]");
|
|
165
172
|
if (copy) {
|
|
166
173
|
await copyText(copy.dataset.copyText);
|
|
@@ -225,7 +232,7 @@ window.LoadToAgentAppFactories.createDialogEventBindings = function createDialog
|
|
|
225
232
|
if (!picker) return;
|
|
226
233
|
if (picker.value) state.agentCommandTargets.set(picker.dataset.agentCommandTarget, picker.value);
|
|
227
234
|
else state.agentCommandTargets.delete(picker.dataset.agentCommandTarget);
|
|
228
|
-
picker.closest("form")?.querySelectorAll("button").forEach(button => { button.disabled = !picker.value; });
|
|
235
|
+
picker.closest("form")?.querySelectorAll("[data-agent-terminal-open], button[type='submit']").forEach(button => { button.disabled = !picker.value; });
|
|
229
236
|
});
|
|
230
237
|
$("#detailDrawer").addEventListener("keydown", (event) => {
|
|
231
238
|
const input = event.target.closest("[data-agent-command-draft]");
|
|
@@ -105,6 +105,13 @@ window.LoadToAgentAppFactories.createNavigationEventBindings = function createNa
|
|
|
105
105
|
buttons[next]?.focus();
|
|
106
106
|
});
|
|
107
107
|
$("#mobileToolsMenu").addEventListener("click", (event) => {
|
|
108
|
+
const guide = event.target.closest("[data-mobile-guide]");
|
|
109
|
+
if (guide) {
|
|
110
|
+
closeMobileTools(false);
|
|
111
|
+
$("#guideBtn")?.click();
|
|
112
|
+
requestAnimationFrame(() => $("#beginnerGuide")?.scrollIntoView({ behavior: "smooth", block: "start" }));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
108
115
|
const button = event.target.closest("[data-mobile-view]");
|
|
109
116
|
if (button) {
|
|
110
117
|
closeMobileTools(false);
|