loadtoagent 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/main.js +53 -8
  2. package/package.json +2 -1
  3. package/renderer/app-agent-actions.js +172 -66
  4. package/renderer/app-bootstrap.js +3 -1
  5. package/renderer/app-dashboard.js +36 -7
  6. package/renderer/app-drawer-content.js +177 -59
  7. package/renderer/app-drawer-data.js +7 -3
  8. package/renderer/app-drawer.js +72 -33
  9. package/renderer/app-events-dialogs.js +8 -1
  10. package/renderer/app-events-navigation.js +7 -0
  11. package/renderer/app-events-sessions.js +136 -13
  12. package/renderer/app-graph-model.js +2 -5
  13. package/renderer/app-graph-orchestration.js +7 -18
  14. package/renderer/app-graph-view.js +194 -48
  15. package/renderer/app-management.js +330 -35
  16. package/renderer/app-quality.js +5 -0
  17. package/renderer/app-session-render.js +20 -83
  18. package/renderer/app.js +5 -1
  19. package/renderer/i18n-messages.js +232 -21
  20. package/renderer/index.html +9 -6
  21. package/renderer/styles-components.css +140 -0
  22. package/renderer/styles-control-room.css +1439 -0
  23. package/renderer/styles-management.css +390 -3
  24. package/renderer/styles-responsive-shell.css +5 -0
  25. package/renderer/styles-runtime-overview.css +20 -0
  26. package/renderer/terminal-agent.js +31 -7
  27. package/renderer/terminal.js +4 -1
  28. package/src/agentMonitor/claudeParser.js +8 -4
  29. package/src/agentMonitor/codexParser.js +5 -4
  30. package/src/agentMonitor/genericParser.js +7 -6
  31. package/src/agentMonitor.js +44 -4
  32. package/src/attentionNotifier.js +3 -0
  33. package/src/ipc/registerTerminalIpc.js +2 -2
  34. package/src/monitorWorker.js +1 -1
  35. package/src/processMonitor.js +68 -7
  36. package/src/terminalHost.js +6 -4
  37. package/src/terminalManager.js +16 -2
@@ -3,74 +3,121 @@
3
3
  window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
4
 
5
5
  window.LoadToAgentAppFactories.createDrawerContent = function createDrawerContent(context = {}) {
6
- const { esc, uiLocale, state, messageContentHtml, compact, fullNumber, timeOnly, providerInfo, statusIcon, agentPathTaskName, snapshotSession } = context;
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 conversation = messages.filter((message) => message.role === "user" || message.role === "assistant");
16
- const activities = messages.filter((message) => message.role !== "user" && message.role !== "assistant");
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 statusLabels = {
23
- started: t("ui.working"), running: t("ui.working"),
24
- done: t("ui.completed"), completed: t("ui.completed"), failed: t("drawer.failed"),
25
- };
26
- const statusLabel = (value) => statusLabels[value] || value || "";
27
- const rows = conversation
28
- .map((message) => {
29
- const role = message.role === "assistant" ? "assistant" : message.role === "tool" ? "tool" : message.role === "system" ? "system" : "user";
30
- const renderedMessage = role === "tool" || role === "system"
31
- ? { ...message, text: window.LoadToAgentI18n.observedText(message.text) }
32
- : message;
33
- const label =
34
- role === "assistant"
35
- ? assistantLabel
36
- : role === "tool"
37
- ? window.LoadToAgentI18n.observedText(message.title || t("session.tool"))
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.conversation_summary", { label: conversationLabel, count: conversation.length, activities: activities.length ? ` · ${t("drawer.activities", { count: activities.length })}` : "" }))}</span>
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}${activityHtml}<div class="chat-latest-anchor" aria-label="${esc(t("drawer.latest_conversation"))}">
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 messages = [...(session.messages || [])];
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 messages = subagentWorkMessages(session);
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-work-source" data-subagent-work-messages="${conversationCount}">
211
- <b>${esc(t("drawer.subagent_work_history"))}</b><span>${esc(sourceCopy)}</span>
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.assignment"),
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
- return { chatHtml, lifecycleHtml, tokensHtml, subagentCommunicationEvents, subagentCoordinationEvents, subagentTextPreview, subagentConversationHtml };
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
- if (!force && detailRequests.has(id)) return detailRequests.get(id).promise;
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
  }
@@ -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.drawerTab = "summary";
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 detailLoading = state.detailLoadingIds.has(state.selectedId);
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 = subagentMode
81
- ? t("drawer.subagent_title", { name: session.agentName || provider.label })
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 = subagentMode ? session.taskName || (session.delegation && session.delegation.taskName) || session.title : session.title;
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 = subagentMode
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
- ${copyTask}${
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
- const rows = [...content.querySelectorAll(".chat-row")];
192
- const latest = rows[rows.length - 1];
193
- if (latest && latest.offsetHeight > content.clientHeight - 90) {
194
- const contentTop = content.getBoundingClientRect().top;
195
- const stickyHeight = content.querySelector(".chat-history-head")?.getBoundingClientRect().height || 0;
196
- content.scrollTop = Math.max(0, content.scrollTop + latest.getBoundingClientRect().top - contentTop - stickyHeight - 12);
197
- } else content.scrollTop = content.scrollHeight;
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);