loadtoagent 1.3.6 → 1.3.8

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