loadtoagent 1.3.11 → 1.3.12
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/package.json +1 -1
- package/renderer/app-dashboard.js +41 -3
- package/renderer/app-drawer-content.js +4 -1
- package/renderer/app-events-filters.js +3 -1
- package/renderer/app-graph-orchestration.js +6 -4
- package/renderer/app-graph-view.js +74 -16
- package/renderer/app-session-render.js +5 -1
- package/renderer/i18n-messages.js +2 -0
- package/renderer/styles-components.css +48 -0
- package/renderer/styles-control-room.css +45 -0
- package/src/agentMonitor/codexParser.js +6 -2
- package/src/agentMonitor/executionActivity.js +3 -4
package/package.json
CHANGED
|
@@ -145,16 +145,53 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
145
145
|
return !isProjectlessSession(session) && projectContainsPath(state.workspace, sessionOriginPath(session));
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function unlinkedLiveTmuxSessions() {
|
|
149
|
+
const displayedSessionIds = new Set(displaySessions().map((session) => String(session.id || "")));
|
|
150
|
+
const allSessionsById = new Map((state.snapshot?.sessions || []).map((session) => [String(session.id || ""), session]));
|
|
151
|
+
const sessions = [];
|
|
152
|
+
for (const distro of state.snapshot?.tmux?.distros || []) {
|
|
153
|
+
for (const tmuxSession of distro.sessions || []) {
|
|
154
|
+
for (const window of tmuxSession.windows || []) {
|
|
155
|
+
for (const pane of window.panes || []) {
|
|
156
|
+
if (!pane.agent || pane.dead || !pane.cwd || !isProviderVisible(pane.agent.provider)) continue;
|
|
157
|
+
const linkedSessionId = String(pane.agent.linkedSessionId || "");
|
|
158
|
+
const linkedSession = linkedSessionId ? allSessionsById.get(linkedSessionId) : null;
|
|
159
|
+
if (linkedSessionId && displayedSessionIds.has(linkedSessionId)) continue;
|
|
160
|
+
// A tmux process can remain alive for days after its linked AI task
|
|
161
|
+
// becomes idle. Do not promote that stale shell back into the Home
|
|
162
|
+
// project count merely because the old conversation aged out of the
|
|
163
|
+
// recent-session list. A still-running linked task remains eligible.
|
|
164
|
+
if (linkedSession && !isControlRoomSession(linkedSession)) continue;
|
|
165
|
+
sessions.push({
|
|
166
|
+
id: `tmux:${pane.id}`,
|
|
167
|
+
provider: pane.agent.provider,
|
|
168
|
+
status: "running",
|
|
169
|
+
originCwd: pane.cwd,
|
|
170
|
+
cwd: pane.cwd,
|
|
171
|
+
workspace: tmuxSession.name,
|
|
172
|
+
title: pane.agent.title || tmuxSession.name,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return sessions;
|
|
179
|
+
}
|
|
180
|
+
|
|
148
181
|
function renderWorkspaces() {
|
|
149
182
|
const rootSessions = displaySessions().filter((session) => !session.parentId);
|
|
150
183
|
const liveRootSessions = rootSessions.filter(isControlRoomSession);
|
|
184
|
+
const tmuxRootSessions = unlinkedLiveTmuxSessions();
|
|
185
|
+
const allLiveRootSessions = [...liveRootSessions, ...tmuxRootSessions];
|
|
151
186
|
const projects = observedProjects(rootSessions);
|
|
152
|
-
const liveProjects = observedProjects(
|
|
187
|
+
const liveProjects = observedProjects(allLiveRootSessions)
|
|
188
|
+
.filter((project) => Number(project.count || 0) > 0);
|
|
153
189
|
const projectlessCount = rootSessions.filter(isProjectlessSession).length;
|
|
154
190
|
const liveProjectlessCount = liveRootSessions.filter(isProjectlessSession).length;
|
|
155
191
|
const savedWorkspaceExists = state.workspace === "all"
|
|
156
192
|
|| (state.workspace === PROJECTLESS_WORKSPACE && projectlessCount > 0)
|
|
157
|
-
|| projects.some((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace))
|
|
193
|
+
|| projects.some((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace))
|
|
194
|
+
|| liveProjects.some((project) => normalizedProjectPath(project.path) === normalizedProjectPath(state.workspace));
|
|
158
195
|
if (!savedWorkspaceExists) state.workspace = "all";
|
|
159
196
|
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" : ""}"
|
|
160
197
|
data-workspace="${esc(item.path)}" title="${esc(item.path)}"
|
|
@@ -188,7 +225,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
188
225
|
const desktopHtml =
|
|
189
226
|
`<button type="button" class="workspace-item control-room-project-chip ${state.workspace === "all" ? "selected" : ""}"
|
|
190
227
|
data-workspace="all" aria-pressed="${state.workspace === "all" ? "true" : "false"}">
|
|
191
|
-
<strong>${esc(t("control.all_projects"))}</strong><small>${
|
|
228
|
+
<strong>${esc(t("control.all_projects"))}</strong><small>${allLiveRootSessions.length}</small>
|
|
192
229
|
</button>` +
|
|
193
230
|
liveProjects.map((item) => projectButton(item, "control-room-project-chip")).join("") +
|
|
194
231
|
(liveProjectlessCount
|
|
@@ -636,6 +673,7 @@ window.LoadToAgentAppFactories.createDashboard = function createDashboard(contex
|
|
|
636
673
|
sessionWorkspaceLabel,
|
|
637
674
|
controlRoomProject,
|
|
638
675
|
matchesWorkspaceFilter,
|
|
676
|
+
unlinkedLiveTmuxSessions,
|
|
639
677
|
renderWorkspaces,
|
|
640
678
|
renderGlobalStats,
|
|
641
679
|
formatBytes,
|
|
@@ -55,8 +55,11 @@ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerConten
|
|
|
55
55
|
const label = assistant ? options.assistantLabel : options.userLabel;
|
|
56
56
|
const avatar = assistant ? providerInfo(session.provider).mark : "ME";
|
|
57
57
|
const fullTime = new Date(message.timestamp).toLocaleString(uiLocale());
|
|
58
|
+
const workingIndicator = options.live
|
|
59
|
+
? `<span class="chat-working-dots" aria-hidden="true"><i></i><i></i><i></i></span>`
|
|
60
|
+
: "";
|
|
58
61
|
const answerKind = assistant && options.answerKind
|
|
59
|
-
? `<span class="chat-answer-kind${options.live ? " is-live" : ""}">${esc(options.answerKind)}</span>`
|
|
62
|
+
? `<span class="chat-answer-kind${options.live ? " is-live" : ""}">${esc(options.answerKind)}${workingIndicator}</span>`
|
|
60
63
|
: "";
|
|
61
64
|
const deliveryStatusKey = {
|
|
62
65
|
sending: "drawer.message_sending",
|
|
@@ -61,7 +61,9 @@ window.LoadToAgentAppFactories.createFilterEventBindings = function createFilter
|
|
|
61
61
|
const item = event.target.closest("[data-workspace]");
|
|
62
62
|
if (item) {
|
|
63
63
|
const label = item.querySelector("strong")?.textContent.trim() || t("project.all");
|
|
64
|
-
state.workspace = item.dataset.workspace
|
|
64
|
+
state.workspace = item.dataset.workspace !== "all" && state.workspace === item.dataset.workspace
|
|
65
|
+
? "all"
|
|
66
|
+
: item.dataset.workspace;
|
|
65
67
|
state.visibleLimit = 30;
|
|
66
68
|
renderWorkspaces();
|
|
67
69
|
renderSessions("filter");
|
|
@@ -17,6 +17,7 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
|
|
|
17
17
|
stableSessionSort = sessions => [...sessions],
|
|
18
18
|
runtimeAgentSummary,
|
|
19
19
|
liveTmuxEntries,
|
|
20
|
+
filteredLiveTmuxEntries,
|
|
20
21
|
runtimeSeparatedOverview,
|
|
21
22
|
focusedGraph,
|
|
22
23
|
scheduleAgentWorkflowConnections,
|
|
@@ -39,6 +40,7 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
|
|
|
39
40
|
&& liveSessionGrid.contains(document.activeElement);
|
|
40
41
|
rememberDisclosureStates(liveSessionGrid);
|
|
41
42
|
const model = connectedGraphSessions(sessions);
|
|
43
|
+
const tmuxEntries = filteredLiveTmuxEntries(model, liveTmuxEntries(state.snapshot && state.snapshot.tmux));
|
|
42
44
|
const focus =
|
|
43
45
|
state.graphFocusId && model.byId.get(state.graphFocusId) && model.included.has(state.graphFocusId) ? model.byId.get(state.graphFocusId) : null;
|
|
44
46
|
if (state.graphFocusId && !focus) state.graphFocusId = null;
|
|
@@ -48,7 +50,7 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
|
|
|
48
50
|
: state.controlRoomSort === "context"
|
|
49
51
|
? [...rootSessions].sort((a, b) => Number((b.context && b.context.percent) || 0) - Number((a.context && a.context.percent) || 0))
|
|
50
52
|
: stableSessionSort(rootSessions);
|
|
51
|
-
if (!model.nodes.length) {
|
|
53
|
+
if (!model.nodes.length && !tmuxEntries.length) {
|
|
52
54
|
if (!preserveFocusedComposer) liveSessionGrid.innerHTML = "";
|
|
53
55
|
$("#graphBreadcrumbs").innerHTML = "";
|
|
54
56
|
$("#graphResetBtn").classList.add("hidden");
|
|
@@ -78,8 +80,8 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
|
|
|
78
80
|
$("#graphResetBtn").classList.remove("hidden");
|
|
79
81
|
scheduleAgentWorkflowConnections();
|
|
80
82
|
} else {
|
|
81
|
-
const runtime = runtimeAgentSummary(model,
|
|
82
|
-
if (!preserveFocusedComposer) liveSessionGrid.innerHTML = runtimeSeparatedOverview(roots, model, roots);
|
|
83
|
+
const runtime = runtimeAgentSummary(model, tmuxEntries);
|
|
84
|
+
if (!preserveFocusedComposer) liveSessionGrid.innerHTML = runtimeSeparatedOverview(roots, model, roots, tmuxEntries);
|
|
83
85
|
restoreDisclosureStates(liveSessionGrid);
|
|
84
86
|
$("#graphBreadcrumbs").innerHTML = "";
|
|
85
87
|
$("#agentMapToolbar")?.classList.add("hidden");
|
|
@@ -87,7 +89,7 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
|
|
|
87
89
|
$("#controlRoomListToolbar")?.classList.remove("hidden");
|
|
88
90
|
syncControlRoomDisclosureButtons();
|
|
89
91
|
$("#graphResetBtn").classList.add("hidden");
|
|
90
|
-
return runtime.activeCount;
|
|
92
|
+
return runtime.activeCount + tmuxEntries.length;
|
|
91
93
|
}
|
|
92
94
|
return model.nodes.filter(isControlRoomSession).length;
|
|
93
95
|
}
|
|
@@ -35,6 +35,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
|
|
|
35
35
|
graphDescendantCount,
|
|
36
36
|
sessionWorkspaceLabel,
|
|
37
37
|
controlRoomProject = session => ({ key: String(session?.workspace || session?.id || "unknown"), label: sessionWorkspaceLabel(session) }),
|
|
38
|
+
matchesWorkspaceFilter = () => true,
|
|
38
39
|
ensureProjectOrder = projectKeys => projectKeys,
|
|
39
40
|
} = context;
|
|
40
41
|
const t = (key, params) => window.LoadToAgentI18n.t(key, params);
|
|
@@ -280,11 +281,49 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
|
|
|
280
281
|
);
|
|
281
282
|
}
|
|
282
283
|
|
|
284
|
+
function tmuxEntrySession(entry) {
|
|
285
|
+
return {
|
|
286
|
+
id: `tmux:${entry?.pane?.id || ""}`,
|
|
287
|
+
provider: entry?.agent?.provider || "",
|
|
288
|
+
status: "running",
|
|
289
|
+
title: entry?.agent?.title || entry?.tmuxSession?.name || "",
|
|
290
|
+
workspace: "",
|
|
291
|
+
originCwd: entry?.pane?.cwd || "",
|
|
292
|
+
cwd: entry?.pane?.cwd || "",
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function filteredLiveTmuxEntries(model, entries = liveTmuxEntries(state.snapshot && state.snapshot.tmux)) {
|
|
297
|
+
const sessionsById = new Map((state.snapshot?.sessions || []).map((session) => [String(session.id || ""), session]));
|
|
298
|
+
const modelSessionIds = new Set((model?.nodes || []).map((session) => String(session.id || "")));
|
|
299
|
+
const query = String(state.search || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
300
|
+
return entries.filter((entry) => {
|
|
301
|
+
const linkedId = String(entry?.agent?.linkedSessionId || "");
|
|
302
|
+
const linkedSession = linkedId ? sessionsById.get(linkedId) : null;
|
|
303
|
+
if (linkedId && modelSessionIds.has(linkedId)) return false;
|
|
304
|
+
if (linkedSession && !isControlRoomSession(linkedSession)) return false;
|
|
305
|
+
const session = tmuxEntrySession(entry);
|
|
306
|
+
if (!session.originCwd || !matchesWorkspaceFilter(session)) return false;
|
|
307
|
+
if (!query) return true;
|
|
308
|
+
return [
|
|
309
|
+
session.title,
|
|
310
|
+
session.workspace,
|
|
311
|
+
session.originCwd,
|
|
312
|
+
entry?.distro?.name,
|
|
313
|
+
entry?.window?.name,
|
|
314
|
+
entry?.pane?.command,
|
|
315
|
+
entry?.agent?.command,
|
|
316
|
+
].join(" ").toLowerCase().includes(query);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
283
320
|
function liveTmuxPaneCard(entry) {
|
|
284
321
|
const { distro, tmuxSession, window, pane, agent } = entry;
|
|
285
322
|
const provider = providerInfo(agent.provider);
|
|
286
323
|
const linked = agent.linkedSessionId ? (state.snapshot?.sessions || []).find((session) => session.id === agent.linkedSessionId) || null : null;
|
|
287
|
-
const title = linked
|
|
324
|
+
const title = linked
|
|
325
|
+
? linked.title
|
|
326
|
+
: agent.title || pane.title || t("graph.tmux_task", { provider: provider.label });
|
|
288
327
|
const stateLabel = pane.dead ? t("graph.ended") : pane.active ? t("graph.selected_pane") : t("graph.background_running");
|
|
289
328
|
return `<article class="live-tmux-card ${pane.active ? "active" : ""} ${pane.dead ? "dead" : ""}"
|
|
290
329
|
style="${providerStyle(agent.provider)}"
|
|
@@ -525,50 +564,69 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
|
|
|
525
564
|
</article>`;
|
|
526
565
|
}
|
|
527
566
|
|
|
528
|
-
function runtimeSeparatedOverview(roots, model, allRoots = roots) {
|
|
567
|
+
function runtimeSeparatedOverview(roots, model, allRoots = roots, tmuxEntries = filteredLiveTmuxEntries(model)) {
|
|
529
568
|
const projectDescriptor = (root) => {
|
|
530
569
|
const project = controlRoomProject(root);
|
|
531
570
|
return { key: project.key, name: project.label };
|
|
532
571
|
};
|
|
572
|
+
const addGroupItem = (groups, key, name, field, value) => {
|
|
573
|
+
if (!groups.has(key)) groups.set(key, { name, roots: [], tmuxEntries: [] });
|
|
574
|
+
groups.get(key)[field].push(value);
|
|
575
|
+
};
|
|
533
576
|
const allGroups = new Map();
|
|
534
577
|
allRoots.forEach((root) => {
|
|
535
578
|
const { key, name } = projectDescriptor(root);
|
|
536
|
-
|
|
537
|
-
allGroups.get(key).roots.push(root);
|
|
579
|
+
addGroupItem(allGroups, key, name, "roots", root);
|
|
538
580
|
});
|
|
539
581
|
const groups = new Map();
|
|
540
582
|
roots.forEach((root) => {
|
|
541
583
|
const { key, name } = projectDescriptor(root);
|
|
542
|
-
|
|
543
|
-
|
|
584
|
+
addGroupItem(groups, key, name, "roots", root);
|
|
585
|
+
});
|
|
586
|
+
tmuxEntries.forEach((entry) => {
|
|
587
|
+
const { key, name } = projectDescriptor(tmuxEntrySession(entry));
|
|
588
|
+
addGroupItem(allGroups, key, name, "tmuxEntries", entry);
|
|
589
|
+
addGroupItem(groups, key, name, "tmuxEntries", entry);
|
|
544
590
|
});
|
|
545
591
|
const defaultOrderedGroups = [...groups.entries()].sort(([leftKey], [rightKey]) =>
|
|
546
|
-
Number(allGroups.get(rightKey)?.roots.length || 0)
|
|
592
|
+
Number((allGroups.get(rightKey)?.roots.length || 0) + (allGroups.get(rightKey)?.tmuxEntries.length || 0))
|
|
593
|
+
- Number((allGroups.get(leftKey)?.roots.length || 0) + (allGroups.get(leftKey)?.tmuxEntries.length || 0)));
|
|
547
594
|
const projectOrder = ensureProjectOrder(defaultOrderedGroups.map(([key]) => key));
|
|
548
595
|
const projectRank = new Map(projectOrder.map((key, index) => [key, index]));
|
|
549
596
|
const orderedGroups = defaultOrderedGroups.sort(([leftKey], [rightKey]) =>
|
|
550
597
|
Number(projectRank.get(leftKey) ?? Number.MAX_SAFE_INTEGER) - Number(projectRank.get(rightKey) ?? Number.MAX_SAFE_INTEGER));
|
|
551
|
-
const projectGroups = orderedGroups.map(([key, { name, roots: projectRoots }], index) => {
|
|
552
|
-
const projectTotals = allGroups.get(key)
|
|
553
|
-
const activeCount = projectTotals.filter((root) => isLiveSession(root)).length;
|
|
554
|
-
const attentionCount = projectTotals.filter((root) =>
|
|
598
|
+
const projectGroups = orderedGroups.map(([key, { name, roots: projectRoots, tmuxEntries: projectTmuxEntries }], index) => {
|
|
599
|
+
const projectTotals = allGroups.get(key) || { roots: projectRoots, tmuxEntries: projectTmuxEntries };
|
|
600
|
+
const activeCount = projectTotals.roots.filter((root) => isLiveSession(root)).length;
|
|
601
|
+
const attentionCount = projectTotals.roots.filter((root) =>
|
|
555
602
|
!isLiveSession(root)
|
|
556
603
|
&& isControlRoomSession(root)
|
|
557
604
|
&& ["waiting", "failed", "paused"].includes(root.status)).length;
|
|
558
|
-
const
|
|
605
|
+
const sessionSummary = attentionCount
|
|
559
606
|
? t("control.project_live_attention_summary", { active: activeCount, attention: attentionCount })
|
|
560
607
|
: t("control.project_live_summary", { active: activeCount });
|
|
608
|
+
const tmuxSummary = projectTotals.tmuxEntries.length
|
|
609
|
+
? t("control.project_tmux_summary", { count: projectTotals.tmuxEntries.length })
|
|
610
|
+
: "";
|
|
611
|
+
const summary = projectTotals.roots.length ? [sessionSummary, tmuxSummary].filter(Boolean).join(" · ") : tmuxSummary;
|
|
561
612
|
const disclosureKey = `control-project:${key}`;
|
|
562
613
|
const presentation = index === 0 ? "is-primary" : "is-secondary";
|
|
563
614
|
const projectFocusId = projectRoots[0]?.id || "";
|
|
615
|
+
const totalCount = projectTotals.roots.length + projectTotals.tmuxEntries.length;
|
|
616
|
+
const tmuxCards = projectTmuxEntries.length
|
|
617
|
+
? `<section class="control-project-tmux-section">
|
|
618
|
+
<header><span><i aria-hidden="true">▦</i><b>${esc(t("graph.tmux_session"))}</b></span><small>${esc(t("control.project_tmux_help"))}</small></header>
|
|
619
|
+
<div class="live-tmux-grid">${projectTmuxEntries.map((entry) => liveTmuxPaneCard(entry)).join("")}</div>
|
|
620
|
+
</section>`
|
|
621
|
+
: "";
|
|
564
622
|
return `<details class="control-room-project-group ${presentation}" data-control-project="${esc(name)}" data-project-sortable="${esc(key)}" data-disclosure-key="${esc(disclosureKey)}">
|
|
565
623
|
<summary class="control-project-header" data-project-toggle="${esc(name)}" draggable="true" aria-grabbed="false"
|
|
566
624
|
aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown" aria-label="${esc(t("project.drag_label", { name }))}" aria-describedby="projectReorderHelp">
|
|
567
|
-
<span class="control-project-heading"><i aria-hidden="true">□</i><b>${esc(name)}</b><small>${esc(summary)}</small><em>${
|
|
625
|
+
<span class="control-project-heading"><i aria-hidden="true">□</i><b>${esc(name)}</b><small>${esc(summary)}</small><em>${totalCount}</em></span>
|
|
568
626
|
<span class="control-project-handle" aria-hidden="true" title="${esc(t("project.reorder_hint"))}"></span>
|
|
569
627
|
</summary>
|
|
570
|
-
|
|
571
|
-
<div class="control-project-body">${projectRoots.map(root => controlRoomSession(root, model)).join("")}</div>
|
|
628
|
+
${projectFocusId ? `<button type="button" class="control-project-flow-link" data-graph-focus="${esc(projectFocusId)}"><span>${esc(t("control.open_full_flow"))} ↗</span></button>` : ""}
|
|
629
|
+
<div class="control-project-body">${projectRoots.map(root => controlRoomSession(root, model)).join("")}${tmuxCards}</div>
|
|
572
630
|
</details>`;
|
|
573
631
|
}).join("");
|
|
574
632
|
return `<div class="control-room-overview" data-control-room-overview="true">
|
|
@@ -892,7 +950,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
|
|
|
892
950
|
}
|
|
893
951
|
|
|
894
952
|
return {
|
|
895
|
-
graphNode, compactGraphNode, providerFlowLane, workflowCompactNode, liveTmuxEntries, liveTmuxPaneCard, runtimeSeparatedOverview,
|
|
953
|
+
graphNode, compactGraphNode, providerFlowLane, workflowCompactNode, liveTmuxEntries, tmuxEntrySession, filteredLiveTmuxEntries, liveTmuxPaneCard, runtimeSeparatedOverview,
|
|
896
954
|
controlRoomIntent, controlRoomSummary, controlRoomAgentGoal, inferredExecutionSummary,
|
|
897
955
|
workflowMetrics, workflowChildrenSummary, splitSubagents, completedSubagentDisclosure, agentPathTaskName, communicationEndpoint,
|
|
898
956
|
workflowCommunicationPanel, executionActivityLabel, executionActivityStatus, executionActivityCard, executionActivityPanel, focusedGraph,
|
|
@@ -168,7 +168,11 @@ window.LoadToAgentAppFactories.createSessionRenderer = function createSessionRen
|
|
|
168
168
|
const graphLiveCount = showMap ? renderAgentMap(graphFilteredSessions(), motionKind) : 0;
|
|
169
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
|
-
const resultCount = attentionView
|
|
171
|
+
const resultCount = attentionView
|
|
172
|
+
? attentionCount
|
|
173
|
+
: state.view === "active"
|
|
174
|
+
? graphLiveCount
|
|
175
|
+
: regular.length;
|
|
172
176
|
$("#sessionResultSummary").textContent = window.LoadToAgentI18n.t("quality.results_summary", { count: resultCount });
|
|
173
177
|
const activeEmpty = state.view === "active" && graphLiveCount === 0;
|
|
174
178
|
$("#activeEmptyState").classList.toggle("hidden", !activeEmpty);
|
|
@@ -1476,6 +1476,8 @@
|
|
|
1476
1476
|
"control.project_filter": {"ko":"프로젝트별 실행 세션 필터","en":"Filter live sessions by project","zh-CN":"按项目筛选运行会话"},
|
|
1477
1477
|
"control.project_live_attention_summary": {"ko":"실행 중 {active} · 확인 필요 {attention}","en":"{active} running · {attention} need attention","zh-CN":"运行中 {active} · 需确认 {attention}"},
|
|
1478
1478
|
"control.project_live_summary": {"ko":"실행 중 {active}","en":"{active} running","zh-CN":"运行中 {active}"},
|
|
1479
|
+
"control.project_tmux_summary": {"ko":"tmux {count}개","en":"{count} tmux","zh-CN":"tmux {count} 个"},
|
|
1480
|
+
"control.project_tmux_help": {"ko":"이 프로젝트 경로에서 직접 감지한 AI 터미널","en":"AI terminals detected directly from this project path","zh-CN":"从此项目路径直接检测到的 AI 终端"},
|
|
1479
1481
|
"control.recent_activity": {"ko":"최근 활동순","en":"Recent activity","zh-CN":"最近活动"},
|
|
1480
1482
|
"control.search_sessions": {"ko":"실행 세션 검색","en":"Search live sessions","zh-CN":"搜索运行会话"},
|
|
1481
1483
|
"control.sort_sessions": {"ko":"실행 세션 정렬","en":"Sort live sessions","zh-CN":"排序运行会话"},
|
|
@@ -1125,6 +1125,9 @@ button[aria-busy="true"] {
|
|
|
1125
1125
|
|
|
1126
1126
|
.chat-answer-kind.is-live {
|
|
1127
1127
|
position: relative;
|
|
1128
|
+
display: inline-flex;
|
|
1129
|
+
align-items: center;
|
|
1130
|
+
gap: 6px;
|
|
1128
1131
|
padding-left: 18px;
|
|
1129
1132
|
border-color: rgba(71,214,153,.3);
|
|
1130
1133
|
background: rgba(34,118,83,.12);
|
|
@@ -1144,6 +1147,51 @@ button[aria-busy="true"] {
|
|
|
1144
1147
|
animation: control-room-pulse 1.35s ease-in-out infinite;
|
|
1145
1148
|
}
|
|
1146
1149
|
|
|
1150
|
+
.chat-working-dots {
|
|
1151
|
+
display: inline-flex;
|
|
1152
|
+
align-items: center;
|
|
1153
|
+
gap: 2px;
|
|
1154
|
+
height: 9px;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.chat-working-dots i {
|
|
1158
|
+
width: 3px;
|
|
1159
|
+
height: 3px;
|
|
1160
|
+
border-radius: 50%;
|
|
1161
|
+
background: currentColor;
|
|
1162
|
+
box-shadow: 0 0 5px rgba(89,223,167,.45);
|
|
1163
|
+
animation: chat-working-dot 1.05s ease-in-out infinite;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.chat-working-dots i:nth-child(2) {
|
|
1167
|
+
animation-delay: 140ms;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.chat-working-dots i:nth-child(3) {
|
|
1171
|
+
animation-delay: 280ms;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
@keyframes chat-working-dot {
|
|
1175
|
+
0%,
|
|
1176
|
+
60%,
|
|
1177
|
+
100% {
|
|
1178
|
+
opacity: .38;
|
|
1179
|
+
transform: translateY(1px);
|
|
1180
|
+
}
|
|
1181
|
+
30% {
|
|
1182
|
+
opacity: 1;
|
|
1183
|
+
transform: translateY(-2px);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1188
|
+
.chat-working-dots i {
|
|
1189
|
+
opacity: .72;
|
|
1190
|
+
animation: none;
|
|
1191
|
+
transform: none;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1147
1195
|
.chat-delivery-progress {
|
|
1148
1196
|
display: grid;
|
|
1149
1197
|
gap: 11px;
|
|
@@ -780,6 +780,51 @@ body details.control-room-project-group > summary.control-project-header {
|
|
|
780
780
|
padding: 9px 10px;
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
+
.control-project-tmux-section {
|
|
784
|
+
padding: 12px 14px 14px;
|
|
785
|
+
border-top: 1px solid rgba(104,130,151,.17);
|
|
786
|
+
background: rgba(8,14,20,.42);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.control-project-tmux-section > header {
|
|
790
|
+
min-height: 28px;
|
|
791
|
+
display: flex;
|
|
792
|
+
align-items: center;
|
|
793
|
+
justify-content: space-between;
|
|
794
|
+
gap: 12px;
|
|
795
|
+
padding: 0 2px 8px;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.control-project-tmux-section > header > span {
|
|
799
|
+
display: inline-flex;
|
|
800
|
+
align-items: center;
|
|
801
|
+
gap: 7px;
|
|
802
|
+
color: #78dfbd;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.control-project-tmux-section > header i {
|
|
806
|
+
font-size: 12px;
|
|
807
|
+
font-style: normal;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.control-project-tmux-section > header b {
|
|
811
|
+
color: #c8ddd6;
|
|
812
|
+
font-size: 12px;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.control-project-tmux-section > header small {
|
|
816
|
+
overflow: hidden;
|
|
817
|
+
color: #708493;
|
|
818
|
+
font-size: 12px;
|
|
819
|
+
text-overflow: ellipsis;
|
|
820
|
+
white-space: nowrap;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.control-project-tmux-section .live-tmux-grid {
|
|
824
|
+
grid-template-columns: repeat(auto-fit,minmax(260px,1fr));
|
|
825
|
+
padding: 0;
|
|
826
|
+
}
|
|
827
|
+
|
|
783
828
|
.control-room-project-group:not([open]) .control-project-header {
|
|
784
829
|
min-height: 50px !important;
|
|
785
830
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { createCodexCollaboration } = require('./codexCollaboration');
|
|
5
|
-
const { createExecutionTracker } = require('./executionActivity');
|
|
5
|
+
const { createExecutionTracker, reconcileExecutionActivities } = require('./executionActivity');
|
|
6
6
|
|
|
7
7
|
const COLLABORATION_TOOLS = new Set([
|
|
8
8
|
'spawn_agent',
|
|
@@ -459,7 +459,11 @@ function createCodexParser(dependencies) {
|
|
|
459
459
|
const collaboration = state.collaboration.finalize(session.collaboration.retainedAgents);
|
|
460
460
|
session.collaboration.spawns = collaboration.spawns;
|
|
461
461
|
session.collaboration.communications = collaboration.communications;
|
|
462
|
-
session.executions = state.executionTracker.finalize()
|
|
462
|
+
session.executions = reconcileExecutionActivities(state.executionTracker.finalize(), {
|
|
463
|
+
staleAfterMs: STALE_TURN_THRESHOLD_MS,
|
|
464
|
+
turnFinished: state.lastTurnCompleted,
|
|
465
|
+
waitingForUser: session.status === 'waiting',
|
|
466
|
+
});
|
|
463
467
|
const windowInfo = modelContextWindow('codex', session.model, state.observedWindow);
|
|
464
468
|
session.context = contextInfo(session.turnUsage.total || session.turnUsage.input, windowInfo);
|
|
465
469
|
|
|
@@ -278,11 +278,10 @@ function reconcileExecutionActivities(activities = [], options = {}) {
|
|
|
278
278
|
if (!activity || activity.status !== 'running') return activity;
|
|
279
279
|
const observedAt = Date.parse(activity.updatedAt || activity.startedAt || 0);
|
|
280
280
|
const observationAge = Number.isFinite(observedAt) ? Math.max(0, now - observedAt) : Number.POSITIVE_INFINITY;
|
|
281
|
+
const observationStale = staleAfterMs > 0 && observationAge >= staleAfterMs;
|
|
281
282
|
const foregroundEnded = activity.mode !== 'background' && turnSettled;
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
&& observationAge >= staleAfterMs;
|
|
285
|
-
if (!foregroundEnded && !backgroundUnobserved) return activity;
|
|
283
|
+
const executionUnobserved = observationStale;
|
|
284
|
+
if (!foregroundEnded && !executionUnobserved) return activity;
|
|
286
285
|
return {
|
|
287
286
|
...activity,
|
|
288
287
|
status: 'unverified',
|