loadtoagent 1.3.4 → 1.3.6

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 (35) hide show
  1. package/docs/PROVIDER-CONTRACTS.md +3 -1
  2. package/package.json +4 -2
  3. package/renderer/app-dashboard.js +75 -13
  4. package/renderer/app-drawer-content.js +189 -8
  5. package/renderer/app-drawer.js +4 -3
  6. package/renderer/app-events-dialogs.js +6 -1
  7. package/renderer/app-events-filters.js +80 -2
  8. package/renderer/app-events-sessions.js +110 -17
  9. package/renderer/app-graph-model.js +8 -5
  10. package/renderer/app-graph-orchestration.js +43 -11
  11. package/renderer/app-graph-view.js +75 -14
  12. package/renderer/app-quality.js +2 -0
  13. package/renderer/app-session-render.js +7 -7
  14. package/renderer/app.js +104 -0
  15. package/renderer/i18n-messages.js +46 -9
  16. package/renderer/index.html +33 -7
  17. package/renderer/styles-components.css +218 -1
  18. package/renderer/styles-control-room.css +820 -29
  19. package/renderer/styles-readability.css +1 -9
  20. package/renderer/styles-responsive-shell.css +22 -1
  21. package/renderer/styles-terminal.css +21 -31
  22. package/renderer/styles-workflow-map.css +8 -0
  23. package/renderer/terminal-events.js +0 -13
  24. package/renderer/terminal-workbench.js +1 -4
  25. package/src/agentMonitor/claudeParser.js +189 -4
  26. package/src/agentMonitor/codexCollaboration.js +1 -0
  27. package/src/agentMonitor/codexParser.js +6 -5
  28. package/src/agentMonitor/executionActivity.js +26 -1
  29. package/src/agentMonitor/hierarchy.js +6 -3
  30. package/src/agentMonitor.js +10 -3
  31. package/src/contracts.js +1 -1
  32. package/src/monitorWorker.js +2 -0
  33. package/src/nodePtyRuntime.js +58 -0
  34. package/src/terminalHost.js +104 -5
  35. package/src/terminalManager.js +5 -1
@@ -9,7 +9,7 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
9
9
  function bindFilterAndWorkspaceEvents() {
10
10
  const syncFilterResetButton = () => {
11
11
  const hasFilters = Boolean(
12
- $("#searchInput").value || state.search || state.providerFilters.size || state.workspace !== "all" || state.sort !== "recent",
12
+ $("#searchInput").value || state.search || state.providerFilters.size || state.workspace !== "all" || state.sort !== "recent" || state.controlRoomSort !== "recent",
13
13
  );
14
14
  $("#resetFiltersBtn").classList.toggle("hidden", !hasFilters);
15
15
  };
@@ -62,6 +62,7 @@ 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;
65
66
  state.visibleLimit = 30;
66
67
  renderWorkspaces();
67
68
  renderSessions("filter");
@@ -85,9 +86,80 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
85
86
  workspaceLists.forEach((list) => {
86
87
  list.addEventListener("click", handleWorkspaceClick);
87
88
  list.addEventListener("keydown", (event) => {
88
- moveFocus(event, event.currentTarget, "[data-workspace]", ["ArrowUp"], ["ArrowDown"]);
89
+ const horizontal = event.currentTarget.id === "workspaceList";
90
+ moveFocus(event, event.currentTarget, "[data-workspace]", horizontal ? ["ArrowLeft", "ArrowUp"] : ["ArrowUp"], horizontal ? ["ArrowRight", "ArrowDown"] : ["ArrowDown"]);
89
91
  });
90
92
  });
93
+ const controlProjectSelect = $("#controlRoomProjectSelect");
94
+ controlProjectSelect?.addEventListener("change", (event) => {
95
+ state.workspace = event.target.value;
96
+ state.controlRoomPage = 0;
97
+ state.visibleLimit = 30;
98
+ renderWorkspaces();
99
+ renderSessions("filter");
100
+ syncFilterResetButton();
101
+ saveDashboardPreferences();
102
+ announce(t("filter.workspace_results", { project: event.target.selectedOptions[0]?.textContent || t("control.all_projects"), count: filteredSessions().length }));
103
+ });
104
+ const controlSortSelect = $("#controlRoomSortSelect");
105
+ controlSortSelect?.addEventListener("change", (event) => {
106
+ state.controlRoomSort = event.target.value;
107
+ state.controlRoomPage = 0;
108
+ state.visibleLimit = 30;
109
+ renderSessions("filter");
110
+ syncFilterResetButton();
111
+ saveDashboardPreferences();
112
+ announce(t("filter.sort_changed", { sort: event.target.selectedOptions[0]?.textContent || event.target.value, count: filteredSessions().length }));
113
+ });
114
+ const controlSearch = $("#controlRoomSearch");
115
+ const controlSearchInput = $("#controlRoomSearchInput");
116
+ const controlSearchButton = $("#controlRoomSearchBtn");
117
+ let controlSearchTimer = null;
118
+ const setControlSearchOpen = (open) => {
119
+ controlSearch?.classList.toggle("is-open", open);
120
+ controlSearchButton?.setAttribute("aria-expanded", open ? "true" : "false");
121
+ if (controlSearchInput) {
122
+ controlSearchInput.tabIndex = open ? 0 : -1;
123
+ controlSearchInput.setAttribute("aria-hidden", open ? "false" : "true");
124
+ }
125
+ if (open) requestAnimationFrame(() => controlSearchInput?.focus());
126
+ };
127
+ controlSearchButton?.addEventListener("click", () => setControlSearchOpen(!controlSearch?.classList.contains("is-open")));
128
+ controlSearchInput?.addEventListener("input", (event) => {
129
+ clearTimeout(controlSearchTimer);
130
+ const value = event.target.value;
131
+ if ($("#searchInput")) $("#searchInput").value = value;
132
+ controlSearchTimer = setTimeout(() => {
133
+ state.search = normalizedSearch(value);
134
+ state.controlRoomPage = 0;
135
+ state.visibleLimit = 30;
136
+ renderSessions("filter");
137
+ syncFilterResetButton();
138
+ saveDashboardPreferences();
139
+ announce(t("filter.search_results", { count: filteredSessions().length }));
140
+ }, 120);
141
+ });
142
+ controlSearchInput?.addEventListener("keydown", (event) => {
143
+ if (event.key !== "Escape") return;
144
+ event.preventDefault();
145
+ if (event.currentTarget.value) {
146
+ event.currentTarget.value = "";
147
+ event.currentTarget.dispatchEvent(new Event("input", { bubbles: true }));
148
+ } else {
149
+ setControlSearchOpen(false);
150
+ controlSearchButton?.focus();
151
+ }
152
+ });
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
+ });
91
163
  let searchTimer = null;
92
164
  $("#searchInput").addEventListener("input", (event) => {
93
165
  clearTimeout(searchTimer);
@@ -96,6 +168,7 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
96
168
  syncFilterResetButton();
97
169
  searchTimer = setTimeout(() => {
98
170
  state.search = normalizedSearch(value);
171
+ state.controlRoomPage = 0;
99
172
  state.visibleLimit = 30;
100
173
  renderSessions("filter");
101
174
  announce(window.LoadToAgentI18n.t("filter.search_results", { count: filteredSessions().length }));
@@ -108,6 +181,7 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
108
181
  $("#searchInput").value = "";
109
182
  $("#searchClearBtn").classList.add("hidden");
110
183
  state.search = "";
184
+ state.controlRoomPage = 0;
111
185
  state.visibleLimit = 30;
112
186
  renderSessions("filter");
113
187
  announce(window.LoadToAgentI18n.t("filter.search_cleared"));
@@ -138,6 +212,7 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
138
212
  const chip = event.target.closest("[data-provider-filter]");
139
213
  if (!chip) return;
140
214
  toggleProviderFilter(chip.dataset.providerFilter);
215
+ state.controlRoomPage = 0;
141
216
  state.visibleLimit = 30;
142
217
  renderProviderFilter();
143
218
  renderProviderOverview();
@@ -154,6 +229,7 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
154
229
  });
155
230
  $("#sortSelect").addEventListener("change", (event) => {
156
231
  state.sort = event.target.value;
232
+ state.controlRoomPage = 0;
157
233
  state.visibleLimit = 30;
158
234
  renderSessions("filter");
159
235
  const label = event.target.selectedOptions[0]?.textContent || event.target.value;
@@ -167,6 +243,8 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
167
243
  state.providerFilters.clear();
168
244
  state.workspace = "all";
169
245
  state.sort = "recent";
246
+ state.controlRoomSort = "recent";
247
+ state.controlRoomPage = 0;
170
248
  state.visibleLimit = 30;
171
249
  $("#searchInput").value = "";
172
250
  $("#searchClearBtn").classList.add("hidden");
@@ -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 };