loadtoagent 1.3.5 → 1.3.7

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.
@@ -10,21 +10,112 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
10
10
  copyText = async () => false,
11
11
  announce = () => {},
12
12
  moveSessionOrder = () => false,
13
+ archiveSession = () => false,
13
14
  } = context;
14
15
 
15
- const moveVisibleSession = (button, container, selector) => {
16
- const nodes = Array.from(container.querySelectorAll(selector));
17
- const current = nodes.findIndex(node => (node.dataset.controlSession || node.dataset.sessionId) === button.dataset.sessionOrderMove);
18
- const offset = Number(button.dataset.sessionOrderOffset || 0);
19
- const target = nodes[current + offset];
20
- const targetId = target && (target.dataset.controlSession || target.dataset.sessionId);
21
- if (current < 0 || !targetId || !moveSessionOrder(button.dataset.sessionOrderMove, targetId, offset > 0)) return;
16
+ let sessionDragJustEnded = false;
17
+
18
+ const sortableSessionId = node => String(node?.dataset.sessionSortable || "");
19
+ const clearSessionDropState = container => {
20
+ container.querySelectorAll("[data-session-sortable]").forEach(node => {
21
+ node.classList.remove("session-sort-dragging");
22
+ node.removeAttribute("data-session-drop-edge");
23
+ node.setAttribute("aria-grabbed", "false");
24
+ });
25
+ };
26
+ const sessionDropPlacement = (target, event) => {
27
+ const bounds = target.getBoundingClientRect();
28
+ const layout = window.getComputedStyle(target.parentElement || target);
29
+ const columns = String(layout.gridTemplateColumns || "none").trim().split(/\s+/).filter(value => value && value !== "none");
30
+ const horizontal = columns.length > 1;
31
+ const placeAfter = horizontal
32
+ ? event.clientX > bounds.left + bounds.width / 2
33
+ : event.clientY > bounds.top + bounds.height / 2;
34
+ return {
35
+ placeAfter,
36
+ edge: horizontal ? (placeAfter ? "right" : "left") : (placeAfter ? "bottom" : "top"),
37
+ };
38
+ };
39
+ const commitSessionPosition = (container, sourceId, targetId, placeAfter, focusSource = false) => {
40
+ if (!moveSessionOrder(sourceId, targetId, placeAfter)) return false;
22
41
  state.sort = "recent";
42
+ state.controlRoomSort = "recent";
23
43
  if ($("#sortSelect")) $("#sortSelect").value = "recent";
44
+ if ($("#controlRoomSortSelect")) $("#controlRoomSortSelect").value = "recent";
24
45
  saveDashboardPreferences();
25
46
  renderSessions("reorder");
26
47
  announce(window.LoadToAgentI18n.t("session.position_changed"));
27
- requestAnimationFrame(() => document.querySelector(`[data-session-order-move="${CSS.escape(button.dataset.sessionOrderMove)}"][data-session-order-offset="${offset}"]`)?.focus({ preventScroll: true }));
48
+ if (focusSource) requestAnimationFrame(() => container.querySelector(`[data-session-sortable="${CSS.escape(sourceId)}"]`)?.focus({ preventScroll: true }));
49
+ return true;
50
+ };
51
+ const bindSortableSessionList = (container, selector) => {
52
+ let draggedSessionId = "";
53
+ const finishDrag = () => {
54
+ clearSessionDropState(container);
55
+ draggedSessionId = "";
56
+ sessionDragJustEnded = true;
57
+ setTimeout(() => { sessionDragJustEnded = false; }, 0);
58
+ };
59
+ container.addEventListener("dragstart", (event) => {
60
+ const item = event.target.closest(selector);
61
+ if (!item) return;
62
+ if (event.target !== item && event.target.closest("button, a, input, select, textarea, summary, [contenteditable='true']")) {
63
+ event.preventDefault();
64
+ return;
65
+ }
66
+ draggedSessionId = sortableSessionId(item);
67
+ if (!draggedSessionId) {
68
+ event.preventDefault();
69
+ return;
70
+ }
71
+ item.classList.add("session-sort-dragging");
72
+ item.setAttribute("aria-grabbed", "true");
73
+ if (event.dataTransfer) {
74
+ event.dataTransfer.effectAllowed = "move";
75
+ event.dataTransfer.setData("text/plain", draggedSessionId);
76
+ event.dataTransfer.setData("application/x-loadtoagent-session-list", container.id);
77
+ const dragImage = item.querySelector(":scope > header, .card-head") || item;
78
+ event.dataTransfer.setDragImage(dragImage, 20, 20);
79
+ }
80
+ });
81
+ container.addEventListener("dragover", (event) => {
82
+ const target = event.target.closest(selector);
83
+ const sourceId = draggedSessionId || event.dataTransfer?.getData("text/plain");
84
+ const sourceList = event.dataTransfer?.getData("application/x-loadtoagent-session-list");
85
+ if (!target || !sourceId || (!draggedSessionId && sourceList !== container.id) || sortableSessionId(target) === sourceId) return;
86
+ event.preventDefault();
87
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
88
+ container.querySelectorAll("[data-session-drop-edge]").forEach(node => node.removeAttribute("data-session-drop-edge"));
89
+ target.dataset.sessionDropEdge = sessionDropPlacement(target, event).edge;
90
+ });
91
+ container.addEventListener("drop", (event) => {
92
+ const target = event.target.closest(selector);
93
+ const sourceId = draggedSessionId || event.dataTransfer?.getData("text/plain");
94
+ const sourceList = event.dataTransfer?.getData("application/x-loadtoagent-session-list");
95
+ if (!target || !sourceId || (!draggedSessionId && sourceList !== container.id) || sortableSessionId(target) === sourceId) return;
96
+ event.preventDefault();
97
+ event.stopPropagation();
98
+ const placement = sessionDropPlacement(target, event);
99
+ const changed = commitSessionPosition(container, sourceId, sortableSessionId(target), placement.placeAfter);
100
+ finishDrag();
101
+ if (!changed) clearSessionDropState(container);
102
+ });
103
+ container.addEventListener("dragend", finishDrag);
104
+ container.addEventListener("dragleave", (event) => {
105
+ if (!container.contains(event.relatedTarget)) clearSessionDropState(container);
106
+ });
107
+ container.addEventListener("keydown", (event) => {
108
+ const item = event.target.closest(selector);
109
+ if (!item || event.target !== item || !event.altKey || !["ArrowUp", "ArrowDown"].includes(event.key)) return;
110
+ const nodes = Array.from(container.querySelectorAll(selector));
111
+ const current = nodes.indexOf(item);
112
+ const offset = event.key === "ArrowUp" ? -1 : 1;
113
+ const target = nodes[current + offset];
114
+ if (current < 0 || !target) return;
115
+ event.preventDefault();
116
+ event.stopPropagation();
117
+ commitSessionPosition(container, sortableSessionId(item), sortableSessionId(target), offset > 0, true);
118
+ });
28
119
  };
29
120
 
30
121
  const managementFilterLabel = value => value === "all" ? window.LoadToAgentI18n.t("management.filter_all") : window.LoadToAgentI18n.t(`management.health.${value}`);
@@ -172,6 +263,7 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
172
263
  }
173
264
 
174
265
  function bindSessionListEvents() {
266
+ bindSortableSessionList($("#sessionGrid"), "[data-session-id][data-session-sortable]");
175
267
  $("#automationOverview").addEventListener("click", (event) => {
176
268
  const loopSelect = event.target.closest("[data-loop-select]");
177
269
  if (loopSelect) {
@@ -228,12 +320,7 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
228
320
  cards[next].focus();
229
321
  });
230
322
  $("#sessionGrid").addEventListener("click", (event) => {
231
- const order = event.target.closest("[data-session-order-move]");
232
- if (order) {
233
- event.stopPropagation();
234
- moveVisibleSession(order, $("#sessionGrid"), "[data-session-id]");
235
- return;
236
- }
323
+ if (sessionDragJustEnded) return;
237
324
  const card = event.target.closest("[data-session-id]");
238
325
  if (card) openDrawer(card.dataset.sessionId);
239
326
  });
@@ -248,11 +335,17 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
248
335
  }
249
336
 
250
337
  function bindLiveAgentEvents() {
338
+ bindSortableSessionList($("#liveSessionGrid"), "[data-control-session][data-session-sortable]");
251
339
  $("#liveSessionGrid").addEventListener("click", async (event) => {
252
- const order = event.target.closest("[data-session-order-move]");
253
- if (order) {
340
+ if (sessionDragJustEnded) return;
341
+ const archive = event.target.closest("[data-session-archive]");
342
+ if (archive) {
254
343
  event.stopPropagation();
255
- moveVisibleSession(order, $("#liveSessionGrid"), "[data-control-session]");
344
+ if (archiveSession(archive.dataset.sessionArchive)) {
345
+ if (state.graphFocusId === archive.dataset.sessionArchive) state.graphFocusId = null;
346
+ renderSessions("archive");
347
+ announce(window.LoadToAgentI18n.t("control.moved_to_history"));
348
+ }
256
349
  return;
257
350
  }
258
351
  const route = event.target.closest("[data-agent-command-route]");
@@ -10,6 +10,7 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
10
10
  state,
11
11
  compact,
12
12
  isLiveSession,
13
+ isControlRoomSession = isLiveSession,
13
14
  stableSessionSort = sessions => [...sessions],
14
15
  } = context;
15
16
 
@@ -27,7 +28,7 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
27
28
 
28
29
  function connectedGraphSessions(sessions, focusId = state.graphFocusId) {
29
30
  const byId = new Map(sessions.map((session) => [session.id, session]));
30
- const included = new Set(sessions.filter(isLiveSession).map((session) => session.id));
31
+ const included = new Set(sessions.filter(isControlRoomSession).map((session) => session.id));
31
32
  const includeAncestors = (session) => {
32
33
  let current = session;
33
34
  const seen = new Set();
@@ -37,7 +38,7 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
37
38
  current = current.parentId ? byId.get(current.parentId) : null;
38
39
  }
39
40
  };
40
- sessions.filter(isLiveSession).forEach(includeAncestors);
41
+ sessions.filter(isControlRoomSession).forEach(includeAncestors);
41
42
  const includeDescendants = (session) => {
42
43
  const queue = [...((session && session.childIds) || [])];
43
44
  const seen = new Set();
@@ -102,6 +103,7 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
102
103
  }
103
104
 
104
105
  function runtimeAgentSummary(model, tmuxEntries = []) {
106
+ const controlNodes = model.nodes.filter(isControlRoomSession);
105
107
  const liveNodes = model.nodes.filter(isLiveSession);
106
108
  const liveById = new Map(liveNodes.map((session) => [session.id, session]));
107
109
  const tmuxSessionIds = new Set(
@@ -114,12 +116,13 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
114
116
  if (agentExecutionMode(session).kind === "tmux") tmuxSessionIds.add(session.id);
115
117
  }
116
118
  const tmuxCount = liveNodes.filter((session) => tmuxSessionIds.has(session.id)).length;
117
- const runningExecutions = liveNodes.flatMap((session) => (session.executions || []).filter((activity) => activity.status === "running"));
119
+ const runningExecutions = controlNodes.flatMap((session) => (session.executions || []).filter((activity) => activity.status === "running"));
118
120
  return {
119
- activeCount: liveNodes.length,
121
+ activeCount: controlNodes.length,
122
+ waitingCount: controlNodes.filter((session) => !isLiveSession(session)).length,
120
123
  standardCount: liveNodes.length - tmuxCount,
121
124
  tmuxCount,
122
- rootCount: liveNodes.filter((session) => !session.parentId).length,
125
+ rootCount: controlNodes.filter((session) => !session.parentId).length,
123
126
  activeHelperCount: liveNodes.filter((session) => session.parentId).length,
124
127
  helperRecordCount: model.nodes.filter((session) => session.parentId).length,
125
128
  runningExecutionCount: runningExecutions.length,
@@ -10,32 +10,52 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
10
10
  readablePreview,
11
11
  agentRoleLabel,
12
12
  isLiveSession,
13
+ isControlRoomSession = isLiveSession,
13
14
  graphPath,
14
15
  connectedGraphSessions,
15
16
  sortGraphNodes,
17
+ stableSessionSort = sessions => [...sessions],
16
18
  runtimeAgentSummary,
17
19
  liveTmuxEntries,
18
20
  runtimeSeparatedOverview,
19
21
  focusedGraph,
20
22
  scheduleAgentWorkflowConnections,
23
+ rememberDisclosureStates = () => {},
24
+ restoreDisclosureStates = () => {},
21
25
  } = context;
22
26
  const t = (key, params) => window.LoadToAgentI18n.t(key, params);
23
27
 
24
28
  function renderAgentMap(sessions, motionKind = "refresh") {
29
+ const liveSessionGrid = $("#liveSessionGrid");
30
+ rememberDisclosureStates(liveSessionGrid);
25
31
  const model = connectedGraphSessions(sessions);
26
32
  const focus =
27
33
  state.graphFocusId && model.byId.get(state.graphFocusId) && model.included.has(state.graphFocusId) ? model.byId.get(state.graphFocusId) : null;
28
34
  if (state.graphFocusId && !focus) state.graphFocusId = null;
29
- const roots = sortGraphNodes(model.nodes.filter((session) => !session.parentId || !model.included.has(session.parentId)));
35
+ const rootSessions = model.nodes.filter((session) => !session.parentId || !model.included.has(session.parentId));
36
+ const roots = state.controlRoomSort === "tokens"
37
+ ? [...rootSessions].sort((a, b) => Number((b.usage && b.usage.total) || 0) - Number((a.usage && a.usage.total) || 0))
38
+ : state.controlRoomSort === "context"
39
+ ? [...rootSessions].sort((a, b) => Number((b.context && b.context.percent) || 0) - Number((a.context && a.context.percent) || 0))
40
+ : stableSessionSort(rootSessions);
30
41
  if (!model.nodes.length) {
31
- $("#liveSessionGrid").innerHTML = "";
42
+ liveSessionGrid.innerHTML = "";
32
43
  $("#graphBreadcrumbs").innerHTML = "";
33
44
  $("#graphResetBtn").classList.add("hidden");
45
+ $("#agentMapToolbar")?.classList.add("hidden");
46
+ $("#controlRoomProjectToolbar")?.classList.remove("hidden");
47
+ $("#controlRoomListToolbar")?.classList.remove("hidden");
48
+ if ($("#controlRoomPageSummary")) $("#controlRoomPageSummary").textContent = t("control.page_summary", { start: 0, end: 0, total: 0 });
49
+ if ($("#controlRoomPagePrev")) $("#controlRoomPagePrev").disabled = true;
50
+ if ($("#controlRoomPageNext")) $("#controlRoomPageNext").disabled = true;
34
51
  return 0;
35
52
  }
36
53
 
37
54
  if (focus) {
38
- $("#liveSessionGrid").innerHTML = focusedGraph(focus, model, motionKind);
55
+ $("#agentMapToolbar")?.classList.remove("hidden");
56
+ $("#controlRoomProjectToolbar")?.classList.add("hidden");
57
+ $("#controlRoomListToolbar")?.classList.add("hidden");
58
+ liveSessionGrid.innerHTML = focusedGraph(focus, model, motionKind);
39
59
  const path = graphPath(focus, model.byId);
40
60
  $("#graphBreadcrumbs").innerHTML = `<button type="button" data-graph-reset>${esc(t("graph.task_list"))}</button>${path
41
61
  .map((item) => {
@@ -51,17 +71,29 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
51
71
  scheduleAgentWorkflowConnections();
52
72
  } else {
53
73
  const runtime = runtimeAgentSummary(model, liveTmuxEntries(state.snapshot && state.snapshot.tmux));
54
- $("#liveSessionGrid").innerHTML = runtimeSeparatedOverview(roots, model);
55
- $("#graphBreadcrumbs").innerHTML = `<span class="control-room-legend">
56
- <span><i class="spawn"></i>${esc(t("control.legend_spawn"))}</span>
57
- <span><i class="running"></i>${esc(t("control.legend_running"))}</span>
58
- <span><i class="done"></i>${esc(t("control.legend_completed"))}</span>
59
- <b>${esc(t("control.live_summary", { sessions: runtime.rootCount, helpers: runtime.activeHelperCount, executions: runtime.runningExecutionCount }))}</b>
60
- </span>`;
74
+ const pageSize = Math.max(1, Number(state.controlRoomPageSize || 4));
75
+ const maxPage = Math.max(0, Math.ceil(roots.length / pageSize) - 1);
76
+ state.controlRoomPage = Math.min(maxPage, Math.max(0, Number(state.controlRoomPage || 0)));
77
+ const startIndex = state.controlRoomPage * pageSize;
78
+ const endIndex = Math.min(roots.length, startIndex + pageSize);
79
+ const visibleRoots = roots.slice(startIndex, endIndex);
80
+ liveSessionGrid.innerHTML = runtimeSeparatedOverview(visibleRoots, model, roots);
81
+ restoreDisclosureStates(liveSessionGrid);
82
+ $("#graphBreadcrumbs").innerHTML = "";
83
+ $("#agentMapToolbar")?.classList.add("hidden");
84
+ $("#controlRoomProjectToolbar")?.classList.remove("hidden");
85
+ $("#controlRoomListToolbar")?.classList.remove("hidden");
86
+ if ($("#controlRoomPageSummary")) $("#controlRoomPageSummary").textContent = t("control.page_summary", {
87
+ start: roots.length ? startIndex + 1 : 0,
88
+ end: endIndex,
89
+ total: roots.length,
90
+ });
91
+ if ($("#controlRoomPagePrev")) $("#controlRoomPagePrev").disabled = state.controlRoomPage === 0;
92
+ if ($("#controlRoomPageNext")) $("#controlRoomPageNext").disabled = state.controlRoomPage >= maxPage;
61
93
  $("#graphResetBtn").classList.add("hidden");
62
94
  return runtime.activeCount;
63
95
  }
64
- return model.nodes.filter(isLiveSession).length;
96
+ return model.nodes.filter(isControlRoomSession).length;
65
97
  }
66
98
 
67
99
  return { renderAgentMap };
@@ -20,6 +20,9 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
20
20
  statusClass,
21
21
  currentActivity,
22
22
  isLiveSession,
23
+ isControlRoomSession = isLiveSession,
24
+ controlRoomStatus = session => session?.status,
25
+ sessionRetentionMinutes = () => 0,
23
26
  subagentWorkState,
24
27
  subagentWorkLabel,
25
28
  latestWorkCopy,
@@ -30,6 +33,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
30
33
  executionModeBadge,
31
34
  graphDescendantCount,
32
35
  sessionWorkspaceLabel,
36
+ controlRoomProject = session => ({ key: String(session?.workspace || session?.id || "unknown"), label: sessionWorkspaceLabel(session) }),
33
37
  } = context;
34
38
  const t = (key, params) => window.LoadToAgentI18n.t(key, params);
35
39
  const statusLabel = (status) => ({
@@ -44,6 +48,8 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
44
48
  const usage = session.usage || {};
45
49
  const percent = Math.max(0, Math.min(100, Number(context.percent || 0)));
46
50
  const running = isLiveSession(session);
51
+ const presentationStatus = controlRoomStatus(session);
52
+ const retained = isControlRoomSession(session) && !running;
47
53
  const childCount = (session.childIds || []).length;
48
54
  const childMetrics = session.collaboration && session.collaboration.metrics;
49
55
  const cumulativeChildren = childMetrics ? childMetrics.cumulativeCreated : childCount;
@@ -72,10 +78,12 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
72
78
  <span class="provider-mark">${esc(provider.mark)}</span>
73
79
  <span class="agent-identity"><b>${esc(role)}</b><small>${esc(provider.label)} · ${esc(session.model || t("graph.model_unknown"))}</small></span>
74
80
  ${executionModeBadge(session, true)}
75
- <span class="status-pill ${statusClass(session.status)}">${esc(statusLabel(session.status))}</span>
81
+ <span class="status-pill ${statusClass(presentationStatus)}">${esc(statusLabel(presentationStatus))}</span>
76
82
  </span>
77
83
  <span class="agent-task-label">
78
- ${session.parentId ? t("graph.assigned_task", { source: delegation.assignmentSource === "parent-narration" ? t("graph.main_ai_explanation_suffix") : "" }) : t("graph.current_goal")}
84
+ ${session.parentId ? t("graph.assigned_task", { source: delegation.assignmentSource === "protected"
85
+ ? t("graph.assignment_protected_suffix")
86
+ : (delegation.assignmentSource === "parent-narration" ? t("graph.main_ai_explanation_suffix") : "") }) : t("graph.current_goal")}
79
87
  </span>
80
88
  <strong class="agent-task" title="${esc(goalPreview.full)}">${esc(goalPreview.text)}</strong>
81
89
  ${goalPreview.truncated ? `<span class="agent-goal-note">${esc(t("graph.summary_shown"))}</span>` : ""}
@@ -103,6 +111,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
103
111
  <span>${session.parentId
104
112
  ? (cumulativeChildren ? t("graph.subagents_created", { count: cumulativeChildren }) : t("graph.helper_ai"))
105
113
  : t("project.origin_named", { name: sessionWorkspaceLabel(session) })}</span>
114
+ ${retained ? `<button type="button" data-session-archive="${esc(session.id)}">${esc(t("control.move_to_history"))}</button>` : ""}
106
115
  <button type="button" data-open-session="${esc(session.id)}">${esc(t("graph.view_conversation"))} <b>↗</b>
107
116
  </button>
108
117
  </footer>
@@ -113,6 +122,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
113
122
  const provider = providerInfo(session.provider);
114
123
  const usage = session.usage || {};
115
124
  const directChildren = graphChildren(session, model).length;
125
+ const presentationStatus = controlRoomStatus(session);
116
126
  const identity = session.parentId
117
127
  ? t("graph.helper_ai_named", { name: session.agentName || agentRoleLabel(session.agentRole) })
118
128
  : t("project.origin_named", { name: sessionWorkspaceLabel(session) });
@@ -124,8 +134,9 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
124
134
  const outcomeText = outcome || latestWorkCopy(session);
125
135
  const assignedWorkPreview = readablePreview(assignedWork, session.parentId ? 110 : 104);
126
136
  const taskLabel = session.parentId ? `${label || agentRoleLabel(session.agentRole)}${taskName ? t("graph.assigned_name_suffix", { name: taskName }) : ""}` : label;
127
- const assignmentSourceNote =
128
- session.parentId && delegation.assignmentSource === "parent-narration"
137
+ const assignmentSourceNote = session.parentId && delegation.assignmentSource === "protected"
138
+ ? `<span class="agent-flow-assignment-source">${esc(t("graph.assignment_source_protected"))}</span>`
139
+ : session.parentId && delegation.assignmentSource === "parent-narration"
129
140
  ? `<span class="agent-flow-assignment-source">${esc(t("graph.main_ai_prestart_explanation"))}</span>`
130
141
  : "";
131
142
  const sharedGoalCopy =
@@ -173,7 +184,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
173
184
  </span>
174
185
  </button>`;
175
186
  }
176
- return `<button type="button" class="agent-flow-row ${isLiveSession(session) ? "running" : ""} ${statusClass(session.status)}"
187
+ return `<button type="button" class="agent-flow-row ${isLiveSession(session) ? "running" : ""} ${statusClass(presentationStatus)}"
177
188
  data-graph-focus="${esc(session.id)}"
178
189
  data-motion-key="agent:${esc(session.id)}"
179
190
  data-motion-value="${esc(session.updatedAt || "")}:${usage.total || 0}:${esc(session.status || "")}"
@@ -185,7 +196,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
185
196
  <em>${esc(identity)} · ${directChildren ? `${t("graph.helper_ai_count", { count: directChildren })} · ` : ""}${esc(timeAgo(session.updatedAt))}</em>
186
197
  ${assignmentSourceNote}${sharedGoalCopy}${outcomeCopy}
187
198
  </span>
188
- <span class="agent-flow-provider"><i>${esc(provider.mark)}</i><small>${esc(statusLabel(session.status))}</small></span>
199
+ <span class="agent-flow-provider"><i>${esc(provider.mark)}</i><small>${esc(statusLabel(presentationStatus))}</small></span>
189
200
  </button>`;
190
201
  }
191
202
 
@@ -418,7 +429,8 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
418
429
  const executionLabel = t("control.runtime_work", { runtime, kind: executionKind });
419
430
  const ownerGoal = controlRoomAgentGoal(owner, 52);
420
431
  const running = activity.status === "running";
421
- return `<button type="button" class="control-room-node execution-node ${running ? "is-running" : "is-complete"}"
432
+ const stateClass = running ? "is-running" : (activity.status === "unverified" ? "is-unverified" : "is-complete");
433
+ return `<button type="button" class="control-room-node execution-node ${stateClass}"
422
434
  data-open-execution-owner="${esc(owner.id)}"
423
435
  data-open-execution-id="${esc(activity.id)}"
424
436
  data-control-summary="${esc(summary.text)}"
@@ -433,10 +445,13 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
433
445
 
434
446
  function controlRoomSession(root, model) {
435
447
  const provider = providerInfo(root.provider);
448
+ const presentationStatus = controlRoomStatus(root);
449
+ const waiting = presentationStatus === "waiting";
450
+ const retained = isControlRoomSession(root) && !isLiveSession(root);
436
451
  const descendants = controlRoomDescendants(root, model);
437
452
  const actors = [root, ...descendants];
438
453
  const executionItems = actors.flatMap(owner => (owner.executions || []).map(activity => ({ activity, owner })));
439
- const activeChildren = descendants.filter(child => !["completed", "cancelled", "failed"].includes(child.status) && !child.completionObserved);
454
+ const activeChildren = descendants.filter(child => ["starting", "running", "paused", "waiting"].includes(child.status) && !child.completionObserved);
440
455
  const completedChildren = descendants.filter(child => ["completed", "cancelled", "failed"].includes(child.status) || child.completionObserved);
441
456
  const activeExecutions = executionItems.filter(item => item.activity.status === "running");
442
457
  const completedExecutions = executionItems
@@ -454,7 +469,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
454
469
  data-control-summary="${esc(title.text)}"
455
470
  data-motion-key="control-main:${esc(root.id)}" data-motion-value="${esc(root.updatedAt || "")}:${esc(root.status || "")}"
456
471
  style="${providerStyle(root.provider)}">
457
- <span class="control-main-top"><span class="provider-mark">${esc(provider.mark)}</span><span><small>${esc(t("control.main_agent"))}</small><b>${esc(provider.label)} · ${esc(root.model || t("session.model_unknown"))}</b></span><em><i aria-hidden="true"></i>${esc(statusLabel(root.status))}</em></span>
472
+ <span class="control-main-top"><span class="provider-mark">${esc(provider.mark)}</span><span><small>${esc(t("control.main_agent"))}</small><b>${esc(provider.label)} · ${esc(root.model || t("session.model_unknown"))}</b></span><em><i aria-hidden="true"></i>${esc(statusLabel(presentationStatus))}</em></span>
458
473
  <strong title="${esc(title.full)}">${esc(title.text)}</strong>
459
474
  <span class="control-main-now"><small>${esc(t("graph.current_work"))}</small><b title="${esc(current.full)}">${esc(current.text)}</b></span>
460
475
  <span class="control-main-meta"><small>${esc(t("control.unit_counts", { helpers: activeChildren.length, executions: activeExecutions.length }))}</small><b>${esc(t("graph.view_conversation"))} →</b></span>
@@ -467,8 +482,16 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
467
482
  const completed = completedUnits.length
468
483
  ? completedUnits.map(unit => unit.kind === "child" ? controlRoomChildNode(unit.child) : controlRoomExecutionNode(unit.item)).join("")
469
484
  : `<div class="control-room-complete-empty"><span>✓</span><small>${esc(t("control.completed_empty"))}</small></div>`;
470
- return `<article class="control-room-session" data-control-session="${esc(root.id)}" style="${providerStyle(root.provider)}">
471
- <header><div><span class="control-session-live"><i></i>${esc(t("control.live_session"))}</span><b>${esc(title.text)}</b></div><span class="session-order-actions control-session-order" role="group" aria-label="${esc(t("session.change_position"))}"><button type="button" data-session-order-move="${esc(root.id)}" data-session-order-offset="-1" title="${esc(t("session.move_up"))}" aria-label="${esc(t("session.move_up"))}">↑</button><button type="button" data-session-order-move="${esc(root.id)}" data-session-order-offset="1" title="${esc(t("session.move_down"))}" aria-label="${esc(t("session.move_down"))}">↓</button></span><button type="button" data-graph-focus="${esc(root.id)}">${esc(t("control.open_full_flow"))} ↗</button></header>
485
+ const waitingWithBackground = waiting && activeExecutions.some(item => item.activity.mode === "background" || item.activity.kind === "background");
486
+ const sessionStateKey = waitingWithBackground
487
+ ? "control.waiting_background_session"
488
+ : (waiting ? "control.waiting_session" : "control.live_session");
489
+ const retention = retained ? `<small class="control-session-retention">${esc(t("control.auto_history_in_minutes", { minutes: sessionRetentionMinutes(root) }))}</small>` : "";
490
+ const archive = retained ? `<button type="button" class="control-session-archive" data-session-archive="${esc(root.id)}">${esc(t("control.move_to_history"))}</button>` : "";
491
+ return `<article class="control-room-session ${waiting ? "is-waiting" : ""} ${waitingWithBackground ? "has-background-work" : ""}" data-control-session="${esc(root.id)}" data-session-sortable="${esc(root.id)}"
492
+ style="${providerStyle(root.provider)}" role="group" tabindex="0" draggable="true" aria-grabbed="false"
493
+ aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown" aria-label="${esc(t("session.drag_label", { title: title.text }))}" aria-describedby="sessionReorderHelp">
494
+ <header><div><span class="control-session-live"><i></i>${esc(t(sessionStateKey))}</span><b>${esc(title.text)}</b>${retention}</div><span class="session-drag-handle" aria-hidden="true" title="${esc(t("session.reorder_hint"))}"></span>${archive}<button type="button" class="control-session-flow" data-graph-focus="${esc(root.id)}">${esc(t("control.open_full_flow"))} ↗</button></header>
472
495
  <div class="control-room-flow">
473
496
  <section class="control-room-column main-column"><span class="control-column-label">${esc(t("control.main_work_column"))}</span>${main}</section>
474
497
  <span class="control-flow-link live" aria-hidden="true"><i></i></span>
@@ -479,9 +502,44 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
479
502
  </article>`;
480
503
  }
481
504
 
482
- function runtimeSeparatedOverview(roots, model) {
505
+ function runtimeSeparatedOverview(roots, model, allRoots = roots) {
506
+ const projectDescriptor = (root) => {
507
+ const project = controlRoomProject(root);
508
+ return { key: project.key, name: project.label };
509
+ };
510
+ const allGroups = new Map();
511
+ allRoots.forEach((root) => {
512
+ const { key, name } = projectDescriptor(root);
513
+ if (!allGroups.has(key)) allGroups.set(key, { name, roots: [] });
514
+ allGroups.get(key).roots.push(root);
515
+ });
516
+ const groups = new Map();
517
+ roots.forEach((root) => {
518
+ const { key, name } = projectDescriptor(root);
519
+ if (!groups.has(key)) groups.set(key, { name, roots: [] });
520
+ groups.get(key).roots.push(root);
521
+ });
522
+ const projectGroups = [...groups.entries()].map(([key, { name, roots: projectRoots }], index) => {
523
+ const projectTotals = allGroups.get(key)?.roots || projectRoots;
524
+ const activeCount = projectTotals.filter((root) => isLiveSession(root)).length;
525
+ const attentionCount = projectTotals.filter((root) => !isLiveSession(root) && isControlRoomSession(root)).length;
526
+ const summary = attentionCount
527
+ ? t("control.project_live_attention_summary", { active: activeCount, attention: attentionCount })
528
+ : t("control.project_live_summary", { active: activeCount });
529
+ const disclosureKey = `control-project:${key}`;
530
+ const presentation = index === 0 ? "is-primary" : "is-secondary";
531
+ const projectFocusId = projectRoots[0]?.id || "";
532
+ return `<details class="control-room-project-group ${presentation}" data-control-project="${esc(name)}" data-disclosure-key="${esc(disclosureKey)}" ${index === 0 ? "open" : ""}>
533
+ <summary class="control-project-header" data-project-toggle="${esc(name)}">
534
+ <span class="control-project-heading"><i aria-hidden="true">□</i><b>${esc(name)}</b><small>${esc(summary)}</small><em>${projectTotals.length}</em></span>
535
+ <span class="control-project-handle" role="img" aria-label="프로젝트 그룹" title="프로젝트 그룹 · 세션은 최근 활동순으로 표시됩니다"></span>
536
+ </summary>
537
+ <button type="button" class="control-project-flow-link" data-graph-focus="${esc(projectFocusId)}"><span>${esc(t("control.open_full_flow"))} ↗</span></button>
538
+ <div class="control-project-body">${projectRoots.map(root => controlRoomSession(root, model)).join("")}</div>
539
+ </details>`;
540
+ }).join("");
483
541
  return `<div class="control-room-overview" data-control-room-overview="true">
484
- ${sortGraphNodes(roots).map(root => controlRoomSession(root, model)).join("")}
542
+ ${projectGroups}
485
543
  </div>`;
486
544
  }
487
545
 
@@ -609,7 +667,9 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
609
667
  : event.kind === "started"
610
668
  ? t("graph.runtime_start_confirmed")
611
669
  : t("graph.status_only_recorded"));
612
- const sourceLabel = event.assignmentSource === "parent-narration" ? ` · ${t("graph.main_ai_prestart_short")}` : "";
670
+ const sourceLabel = event.assignmentSource === "protected"
671
+ ? ` · ${t("graph.assignment_source_protected_short")}`
672
+ : (event.assignmentSource === "parent-narration" ? ` · ${t("graph.main_ai_prestart_short")}` : "");
613
673
  return `<article class="agent-communication-event ${esc(event.kind)}" data-communication-kind="${esc(event.kind)}">
614
674
  <span class="communication-route">
615
675
  <b>${esc(communicationEndpoint(event.from, owner, model))}</b><i>→</i>
@@ -650,6 +710,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
650
710
  running: t("graph.execution_running"),
651
711
  completed: t("graph.execution_completed"),
652
712
  failed: t("graph.execution_failed"),
713
+ unverified: t("graph.execution_unverified"),
653
714
  cancelled: t("ui.stopped"),
654
715
  })[activity.status] || activity.status;
655
716
  }
@@ -94,6 +94,7 @@ window.LoadToAgentAppFactories.createQualityEnhancements = function createQualit
94
94
  ? dashboard.workspace
95
95
  : "all";
96
96
  state.sort = ["recent", "tokens", "context"].includes(dashboard.sort) ? dashboard.sort : "recent";
97
+ state.controlRoomSort = ["recent", "tokens", "context"].includes(dashboard.controlRoomSort) ? dashboard.controlRoomSort : "recent";
97
98
  state.sessionOrder = Array.isArray(dashboard.sessionOrder)
98
99
  ? dashboard.sessionOrder.filter(id => typeof id === "string" && id.length <= 500).slice(0, 1_000)
99
100
  : [];
@@ -130,6 +131,7 @@ window.LoadToAgentAppFactories.createQualityEnhancements = function createQualit
130
131
  providers: [...state.providerFilters],
131
132
  workspace: String(state.workspace || "all").slice(0, 2_000),
132
133
  sort: ["recent", "tokens", "context"].includes(state.sort) ? state.sort : "recent",
134
+ controlRoomSort: ["recent", "tokens", "context"].includes(state.controlRoomSort) ? state.controlRoomSort : "recent",
133
135
  sessionOrder: (state.sessionOrder || []).filter(id => typeof id === "string" && id.length <= 500).slice(0, 1_000),
134
136
  };
135
137
  try {
@@ -24,6 +24,7 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
24
24
  statusClass,
25
25
  currentActivity,
26
26
  isLiveSession,
27
+ isControlRoomSession = isLiveSession,
27
28
  latestWorkCopy,
28
29
  statusIcon,
29
30
  renderProviderRail,
@@ -75,11 +76,13 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
75
76
  const originLabel = sessionWorkspaceLabel(session);
76
77
  return `<article class="session-card session-record ${opts.live ? "live-card" : ""} ${statusClass(session.status)} ${session.parentId ? "subagent" : ""}"
77
78
  data-session-id="${esc(session.id)}"
79
+ data-session-sortable="${esc(session.id)}"
78
80
  data-motion-key="session:${esc(session.id)}"
79
81
  data-motion-value="${esc(session.updatedAt || "")}:${esc(session.status || "")}"
80
82
  style="${providerStyle(session.provider)}"
81
- role="button" tabindex="0"
82
- aria-labelledby="${accessibleId}-title" aria-describedby="${accessibleId}-summary">
83
+ role="button" tabindex="0" draggable="true" aria-grabbed="false"
84
+ aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown"
85
+ aria-labelledby="${accessibleId}-title" aria-describedby="${accessibleId}-summary sessionReorderHelp">
83
86
  <div class="card-head">
84
87
  <span class="provider-mark">${esc(provider.mark)}</span>
85
88
  <div class="card-head-main"><div class="card-provider-line"><b>${esc(provider.label)}</b><span>${esc(session.model || t("session.model_unknown"))}</span></div></div>
@@ -95,10 +98,7 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
95
98
  </div>
96
99
  <footer class="card-footer">
97
100
  <span>${esc(timeAgo(session.updatedAt))}</span>
98
- <span class="session-order-actions" role="group" aria-label="${esc(t("session.change_position"))}">
99
- <button type="button" data-session-order-move="${esc(session.id)}" data-session-order-offset="-1" title="${esc(t("session.move_up"))}" aria-label="${esc(t("session.move_up"))}">↑</button>
100
- <button type="button" data-session-order-move="${esc(session.id)}" data-session-order-offset="1" title="${esc(t("session.move_down"))}" aria-label="${esc(t("session.move_down"))}">↓</button>
101
- </span>
101
+ <span class="session-drag-handle" aria-hidden="true" title="${esc(t("session.reorder_hint"))}"></span>
102
102
  <strong>${esc(t("graph.view_conversation"))}<i aria-hidden="true">→</i></strong>
103
103
  </footer>
104
104
  </article>`;
@@ -166,7 +166,7 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
166
166
  const attentionCount = attentionView ? renderAttentionInbox() : 0;
167
167
  const showMap = ["all", "active"].includes(state.view);
168
168
  const graphLiveCount = showMap ? renderAgentMap(graphFilteredSessions(), motionKind) : 0;
169
- const regular = state.view === "all" ? sessions.filter((session) => !isLiveSession(session)) : state.view === "active" ? [] : sessions;
169
+ const regular = state.view === "all" ? sessions.filter((session) => !isControlRoomSession(session)) : state.view === "active" ? [] : sessions;
170
170
  const visible = regular.slice(0, state.visibleLimit);
171
171
  const resultCount = attentionView ? attentionCount : graphLiveCount + regular.length;
172
172
  $("#sessionResultSummary").textContent = window.LoadToAgentI18n.t("quality.results_summary", { count: resultCount });