loadtoagent 1.0.0
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/README.ko.md +175 -0
- package/README.md +175 -0
- package/README.zh-CN.md +175 -0
- package/bin/loadtoagent.js +171 -0
- package/docs/ARCHITECTURE.md +30 -0
- package/docs/PROVIDER-CONTRACTS.md +58 -0
- package/docs/assets/loadtoagent-dashboard.png +0 -0
- package/docs/assets/loadtoagent-demo.gif +0 -0
- package/main.js +493 -0
- package/package.json +133 -0
- package/preload.js +75 -0
- package/renderer/app-agent-actions.js +285 -0
- package/renderer/app-bootstrap.js +95 -0
- package/renderer/app-dashboard.js +361 -0
- package/renderer/app-drawer-content.js +203 -0
- package/renderer/app-drawer-data.js +41 -0
- package/renderer/app-drawer.js +183 -0
- package/renderer/app-events-dialogs.js +169 -0
- package/renderer/app-events-filters.js +74 -0
- package/renderer/app-events-navigation.js +96 -0
- package/renderer/app-events-sessions.js +195 -0
- package/renderer/app-events.js +16 -0
- package/renderer/app-graph-layout.js +71 -0
- package/renderer/app-graph-model.js +107 -0
- package/renderer/app-graph-orchestration.js +76 -0
- package/renderer/app-graph-view.js +544 -0
- package/renderer/app-run-modal.js +221 -0
- package/renderer/app-session-render.js +243 -0
- package/renderer/app-tmux-render.js +247 -0
- package/renderer/app.js +608 -0
- package/renderer/fonts/geist-ext.woff2 +0 -0
- package/renderer/fonts/geist.woff2 +0 -0
- package/renderer/i18n-messages.js +355 -0
- package/renderer/i18n.js +122 -0
- package/renderer/index.html +386 -0
- package/renderer/shared.js +26 -0
- package/renderer/styles-agent-map.css +401 -0
- package/renderer/styles-cards.css +600 -0
- package/renderer/styles-collaboration.css +534 -0
- package/renderer/styles-components.css +1293 -0
- package/renderer/styles-onboarding.css +344 -0
- package/renderer/styles-overlays.css +284 -0
- package/renderer/styles-product.css +686 -0
- package/renderer/styles-responsive-product.css +689 -0
- package/renderer/styles-responsive-runtime.css +718 -0
- package/renderer/styles-responsive-shell.css +533 -0
- package/renderer/styles-responsive-workflows.css +778 -0
- package/renderer/styles-run-composer.css +695 -0
- package/renderer/styles-settings.css +606 -0
- package/renderer/styles-terminal.css +1241 -0
- package/renderer/styles-tmux.css +1162 -0
- package/renderer/styles-workflow-map.css +513 -0
- package/renderer/styles-workflows.css +1217 -0
- package/renderer/styles.css +486 -0
- package/renderer/terminal-agent.js +152 -0
- package/renderer/terminal-events.js +226 -0
- package/renderer/terminal-workbench.js +464 -0
- package/renderer/terminal.js +497 -0
- package/src/agentMonitor/claudeParser.js +197 -0
- package/src/agentMonitor/codexCollaboration.js +95 -0
- package/src/agentMonitor/codexParser.js +447 -0
- package/src/agentMonitor/genericParser.js +244 -0
- package/src/agentMonitor/hierarchy.js +248 -0
- package/src/agentMonitor/sessionFiles.js +86 -0
- package/src/agentMonitor.js +591 -0
- package/src/agentRunner.js +465 -0
- package/src/bridgeServer.js +246 -0
- package/src/contracts.js +71 -0
- package/src/diagnostics.js +23 -0
- package/src/ipc/registerAgentIpc.js +12 -0
- package/src/ipc/registerAppIpc.js +20 -0
- package/src/ipc/registerTerminalIpc.js +50 -0
- package/src/ipc/registerTmuxIpc.js +30 -0
- package/src/ipc/registerWorkspaceIpc.js +14 -0
- package/src/monitorWorker.js +273 -0
- package/src/processMonitor.js +394 -0
- package/src/providerRegistry.js +120 -0
- package/src/terminalManager.js +438 -0
- package/src/tmuxController.js +183 -0
- package/src/tmuxMonitor.js +432 -0
- package/src/updateManager.js +339 -0
- package/src/workspaceStore.js +77 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
4
|
+
|
|
5
|
+
window.LoadToAgentAppFactories.createSessionEventBindings = function createSessionEventBindings(context = {}) {
|
|
6
|
+
const {
|
|
7
|
+
$, state, selectView, renderProviderOverview, renderProviderFilter, toggleProviderFilter, announceProviderFilter, renderSessions, renderTmuxMap, openDrawer, openSubagentConversation,
|
|
8
|
+
dispatchAgentCommand, openAgentTerminal, copyBridgeCommand, openSessionOrigin,
|
|
9
|
+
} = context;
|
|
10
|
+
|
|
11
|
+
function bindSessionListEvents() {
|
|
12
|
+
$("#providerOverview").addEventListener("click", (event) => {
|
|
13
|
+
const card = event.target.closest("[data-provider-card]");
|
|
14
|
+
if (!card) return;
|
|
15
|
+
toggleProviderFilter(card.dataset.providerCard);
|
|
16
|
+
state.visibleLimit = 30;
|
|
17
|
+
renderProviderFilter();
|
|
18
|
+
renderProviderOverview();
|
|
19
|
+
renderSessions("filter");
|
|
20
|
+
announceProviderFilter();
|
|
21
|
+
requestAnimationFrame(() => $("#providerOverview").querySelector(`[data-provider-card="${CSS.escape(card.dataset.providerCard)}"]`)?.focus());
|
|
22
|
+
});
|
|
23
|
+
$("#sessionGrid").addEventListener("click", (event) => {
|
|
24
|
+
const card = event.target.closest("[data-session-id]");
|
|
25
|
+
if (card) openDrawer(card.dataset.sessionId);
|
|
26
|
+
});
|
|
27
|
+
$("#sessionGrid").addEventListener("keydown", (event) => {
|
|
28
|
+
const card = event.target.closest("[data-session-id]");
|
|
29
|
+
if (card && (event.key === "Enter" || event.key === " ")) {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
openDrawer(card.dataset.sessionId);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function bindLiveAgentEvents() {
|
|
37
|
+
$("#liveSessionGrid").addEventListener("click", (event) => {
|
|
38
|
+
const tmuxPane = event.target.closest('.live-tmux-pane[data-tmux-type="pane"][data-tmux-id]');
|
|
39
|
+
const tmuxOverview = event.target.closest(".live-tmux-overview-open");
|
|
40
|
+
if (tmuxPane || tmuxOverview) {
|
|
41
|
+
event.stopPropagation();
|
|
42
|
+
if (tmuxPane) state.tmuxFocus = { type: "pane", id: tmuxPane.dataset.tmuxId };
|
|
43
|
+
selectView("tmux");
|
|
44
|
+
if (tmuxPane) window.LoadToAgentTerminal?.selectTmuxById(tmuxPane.dataset.tmuxId);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const bridge = event.target.closest("[data-agent-bridge-copy]");
|
|
48
|
+
if (bridge) {
|
|
49
|
+
event.stopPropagation();
|
|
50
|
+
copyBridgeCommand(bridge.dataset.agentBridgeCopy);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const origin = event.target.closest("[data-agent-open-origin]");
|
|
54
|
+
if (origin) {
|
|
55
|
+
event.stopPropagation();
|
|
56
|
+
openSessionOrigin(origin.dataset.agentOpenOrigin);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const terminal = event.target.closest("[data-agent-terminal-open]");
|
|
60
|
+
if (terminal) {
|
|
61
|
+
event.stopPropagation();
|
|
62
|
+
openAgentTerminal(terminal.dataset.agentTerminalOpen);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const completedToggle = event.target.closest("[data-subagent-completed-toggle]");
|
|
66
|
+
if (completedToggle) {
|
|
67
|
+
const ownerId = completedToggle.dataset.subagentCompletedToggle;
|
|
68
|
+
if (state.expandedCompletedSubagents.has(ownerId)) state.expandedCompletedSubagents.delete(ownerId);
|
|
69
|
+
else state.expandedCompletedSubagents.add(ownerId);
|
|
70
|
+
renderSessions("expand");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const more = event.target.closest("[data-graph-provider-more]");
|
|
74
|
+
if (more) {
|
|
75
|
+
state.graphExpandedProviders.add(more.dataset.graphProviderMore);
|
|
76
|
+
renderSessions("expand");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const less = event.target.closest("[data-graph-provider-less]");
|
|
80
|
+
if (less) {
|
|
81
|
+
state.graphExpandedProviders.delete(less.dataset.graphProviderLess);
|
|
82
|
+
renderSessions("expand");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const open = event.target.closest("[data-open-session]");
|
|
86
|
+
if (open) {
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
openDrawer(open.dataset.openSession);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const subagentChat = event.target.closest("[data-open-subagent-chat]");
|
|
92
|
+
if (subagentChat) {
|
|
93
|
+
event.stopPropagation();
|
|
94
|
+
openSubagentConversation(subagentChat.dataset.openSubagentChat);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const node = event.target.closest("[data-graph-focus]");
|
|
98
|
+
if (!node) return;
|
|
99
|
+
if (state.graphFocusId === node.dataset.graphFocus) openDrawer(node.dataset.graphFocus);
|
|
100
|
+
else {
|
|
101
|
+
state.graphFocusId = node.dataset.graphFocus;
|
|
102
|
+
renderSessions("focus");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
$("#liveSessionGrid").addEventListener("input", (event) => {
|
|
106
|
+
const input = event.target.closest("[data-agent-command-draft]");
|
|
107
|
+
if (input) state.agentCommandDrafts.set(input.dataset.agentCommandDraft, input.value);
|
|
108
|
+
});
|
|
109
|
+
$("#liveSessionGrid").addEventListener("change", (event) => {
|
|
110
|
+
const picker = event.target.closest("[data-agent-command-target]");
|
|
111
|
+
if (!picker) return;
|
|
112
|
+
if (picker.value) state.agentCommandTargets.set(picker.dataset.agentCommandTarget, picker.value);
|
|
113
|
+
else state.agentCommandTargets.delete(picker.dataset.agentCommandTarget);
|
|
114
|
+
const form = picker.closest("[data-agent-command-form]");
|
|
115
|
+
const enabled = Boolean(picker.value);
|
|
116
|
+
form &&
|
|
117
|
+
form.querySelectorAll("button").forEach((button) => {
|
|
118
|
+
button.disabled = !enabled;
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
$("#liveSessionGrid").addEventListener("keydown", (event) => {
|
|
122
|
+
const input = event.target.closest("[data-agent-command-draft]");
|
|
123
|
+
if (!input || event.key !== "Enter" || event.shiftKey || event.isComposing || event.keyCode === 229) return;
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
input.closest("form")?.requestSubmit();
|
|
126
|
+
});
|
|
127
|
+
$("#liveSessionGrid").addEventListener("submit", (event) => {
|
|
128
|
+
const form = event.target.closest("[data-agent-command-form]");
|
|
129
|
+
if (!form) return;
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
dispatchAgentCommand(form.dataset.agentCommandForm, form);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function bindGraphNavigationEvents() {
|
|
136
|
+
$("#graphBreadcrumbs").addEventListener("click", (event) => {
|
|
137
|
+
if (event.target.closest("[data-graph-reset]")) state.graphFocusId = null;
|
|
138
|
+
else {
|
|
139
|
+
const node = event.target.closest("[data-graph-focus]");
|
|
140
|
+
if (!node) return;
|
|
141
|
+
state.graphFocusId = node.dataset.graphFocus;
|
|
142
|
+
}
|
|
143
|
+
renderSessions("focus-back");
|
|
144
|
+
});
|
|
145
|
+
$("#graphResetBtn").addEventListener("click", () => {
|
|
146
|
+
state.graphFocusId = null;
|
|
147
|
+
renderSessions("focus-back");
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function bindTmuxMapEvents() {
|
|
152
|
+
$("#tmuxMap").addEventListener("click", (event) => {
|
|
153
|
+
const control = event.target.closest("[data-control-tmux]");
|
|
154
|
+
if (control) {
|
|
155
|
+
event.stopPropagation();
|
|
156
|
+
window.LoadToAgentTerminal?.selectTmuxById(control.dataset.controlTmux);
|
|
157
|
+
$("#tmuxControlSection").scrollIntoView({ behavior: "smooth", block: "start" });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const open = event.target.closest("[data-open-session]");
|
|
161
|
+
if (open) {
|
|
162
|
+
event.stopPropagation();
|
|
163
|
+
openDrawer(open.dataset.openSession);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const node = event.target.closest("[data-tmux-type][data-tmux-id]");
|
|
167
|
+
if (!node) return;
|
|
168
|
+
state.tmuxFocus = { type: node.dataset.tmuxType, id: node.dataset.tmuxId };
|
|
169
|
+
renderTmuxMap();
|
|
170
|
+
if (node.dataset.tmuxType === "pane") window.LoadToAgentTerminal?.selectTmuxById(node.dataset.tmuxId);
|
|
171
|
+
});
|
|
172
|
+
$("#tmuxBreadcrumbs").addEventListener("click", (event) => {
|
|
173
|
+
if (event.target.closest("[data-tmux-reset]")) state.tmuxFocus = null;
|
|
174
|
+
else {
|
|
175
|
+
const node = event.target.closest("[data-tmux-type][data-tmux-id]");
|
|
176
|
+
if (!node) return;
|
|
177
|
+
state.tmuxFocus = { type: node.dataset.tmuxType, id: node.dataset.tmuxId };
|
|
178
|
+
}
|
|
179
|
+
renderTmuxMap();
|
|
180
|
+
});
|
|
181
|
+
$("#tmuxResetBtn").addEventListener("click", () => {
|
|
182
|
+
state.tmuxFocus = null;
|
|
183
|
+
renderTmuxMap();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function bindSessionAndAgentEvents() {
|
|
188
|
+
bindSessionListEvents();
|
|
189
|
+
bindLiveAgentEvents();
|
|
190
|
+
bindGraphNavigationEvents();
|
|
191
|
+
bindTmuxMapEvents();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { bindSessionAndAgentEvents };
|
|
195
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
4
|
+
|
|
5
|
+
window.LoadToAgentAppFactories.createEventBindings = function createEventBindings(context = {}) {
|
|
6
|
+
const { bindNavigationAndUpdateEvents, bindSessionAndAgentEvents, bindFilterAndWorkspaceEvents, bindDialogAndGlobalEvents } = context;
|
|
7
|
+
|
|
8
|
+
function bindEvents() {
|
|
9
|
+
bindNavigationAndUpdateEvents();
|
|
10
|
+
bindSessionAndAgentEvents();
|
|
11
|
+
bindFilterAndWorkspaceEvents();
|
|
12
|
+
bindDialogAndGlobalEvents();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return { bindEvents };
|
|
16
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
4
|
+
|
|
5
|
+
window.LoadToAgentAppFactories.createGraphLayout = function createGraphLayout(context = {}) {
|
|
6
|
+
const {
|
|
7
|
+
$,
|
|
8
|
+
} = context;
|
|
9
|
+
|
|
10
|
+
let agentWorkflowFrame = 0;
|
|
11
|
+
|
|
12
|
+
function workflowPortPoint(port, canvasRect) {
|
|
13
|
+
const rect = port.getBoundingClientRect();
|
|
14
|
+
return { x: rect.left - canvasRect.left + rect.width / 2, y: rect.top - canvasRect.top + rect.height / 2 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function workflowCurve(from, to) {
|
|
18
|
+
const horizontal = Math.abs(to.x - from.x) >= Math.abs(to.y - from.y);
|
|
19
|
+
if (horizontal) {
|
|
20
|
+
const distance = Math.max(34, Math.abs(to.x - from.x) * 0.48);
|
|
21
|
+
return [
|
|
22
|
+
`M ${from.x.toFixed(1)} ${from.y.toFixed(1)}`,
|
|
23
|
+
`C ${(from.x + distance).toFixed(1)} ${from.y.toFixed(1)},`,
|
|
24
|
+
`${(to.x - distance).toFixed(1)} ${to.y.toFixed(1)},`,
|
|
25
|
+
`${to.x.toFixed(1)} ${to.y.toFixed(1)}`,
|
|
26
|
+
].join(" ");
|
|
27
|
+
}
|
|
28
|
+
const distance = Math.max(34, Math.abs(to.y - from.y) * 0.48);
|
|
29
|
+
return [
|
|
30
|
+
`M ${from.x.toFixed(1)} ${from.y.toFixed(1)}`,
|
|
31
|
+
`C ${from.x.toFixed(1)} ${(from.y + distance).toFixed(1)},`,
|
|
32
|
+
`${to.x.toFixed(1)} ${(to.y - distance).toFixed(1)},`,
|
|
33
|
+
`${to.x.toFixed(1)} ${to.y.toFixed(1)}`,
|
|
34
|
+
].join(" ");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function drawAgentWorkflowConnections() {
|
|
38
|
+
const canvas = document.querySelector(".agent-workflow-canvas");
|
|
39
|
+
if (!canvas || !canvas.isConnected) return;
|
|
40
|
+
const svg = canvas.querySelector(".agent-workflow-edges");
|
|
41
|
+
const paths = svg && svg.querySelector("[data-workflow-paths]");
|
|
42
|
+
const upstream = canvas.querySelector('[data-workflow-port="upstream-output"]');
|
|
43
|
+
const focusInput = canvas.querySelector('[data-workflow-port="focus-input"]');
|
|
44
|
+
const focusOutput = canvas.querySelector('[data-workflow-port="focus-output"]');
|
|
45
|
+
const childrenGroupInput = canvas.querySelector('[data-workflow-port="children-group-input"]');
|
|
46
|
+
if (!svg || !paths || !upstream || !focusInput) return;
|
|
47
|
+
const rect = canvas.getBoundingClientRect();
|
|
48
|
+
svg.setAttribute("viewBox", `0 0 ${Math.max(1, rect.width)} ${Math.max(1, rect.height)}`);
|
|
49
|
+
const upstreamPath = `<path class="agent-workflow-edge upstream branch"
|
|
50
|
+
data-workflow-edge-kind="upstream" pathLength="1"
|
|
51
|
+
d="${workflowCurve(workflowPortPoint(upstream, rect), workflowPortPoint(focusInput, rect))}"
|
|
52
|
+
marker-end="url(#workflowArrow)">
|
|
53
|
+
</path>`;
|
|
54
|
+
const downstreamPath =
|
|
55
|
+
focusOutput && childrenGroupInput
|
|
56
|
+
? `<path class="agent-workflow-edge downstream group"
|
|
57
|
+
data-workflow-edge-kind="children-group" pathLength="1"
|
|
58
|
+
d="${workflowCurve(workflowPortPoint(focusOutput, rect), workflowPortPoint(childrenGroupInput, rect))}"
|
|
59
|
+
marker-end="url(#workflowArrow)">
|
|
60
|
+
</path>`
|
|
61
|
+
: "";
|
|
62
|
+
paths.innerHTML = `${upstreamPath}${downstreamPath}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function scheduleAgentWorkflowConnections() {
|
|
66
|
+
cancelAnimationFrame(agentWorkflowFrame);
|
|
67
|
+
agentWorkflowFrame = requestAnimationFrame(() => requestAnimationFrame(drawAgentWorkflowConnections));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { agentWorkflowFrame, workflowPortPoint, workflowCurve, drawAgentWorkflowConnections, scheduleAgentWorkflowConnections };
|
|
71
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
4
|
+
|
|
5
|
+
window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(context = {}) {
|
|
6
|
+
const {
|
|
7
|
+
$,
|
|
8
|
+
esc,
|
|
9
|
+
state,
|
|
10
|
+
compact,
|
|
11
|
+
isLiveSession,
|
|
12
|
+
} = context;
|
|
13
|
+
|
|
14
|
+
function graphPath(session, byId) {
|
|
15
|
+
const path = [];
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
let current = session;
|
|
18
|
+
while (current && !seen.has(current.id)) {
|
|
19
|
+
path.unshift(current);
|
|
20
|
+
seen.add(current.id);
|
|
21
|
+
current = current.parentId ? byId.get(current.parentId) : null;
|
|
22
|
+
}
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function connectedGraphSessions(sessions, focusId = state.graphFocusId) {
|
|
27
|
+
const byId = new Map(sessions.map((session) => [session.id, session]));
|
|
28
|
+
const included = new Set(sessions.filter(isLiveSession).map((session) => session.id));
|
|
29
|
+
const includeAncestors = (session) => {
|
|
30
|
+
let current = session;
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
while (current && !seen.has(current.id)) {
|
|
33
|
+
included.add(current.id);
|
|
34
|
+
seen.add(current.id);
|
|
35
|
+
current = current.parentId ? byId.get(current.parentId) : null;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
sessions.filter(isLiveSession).forEach(includeAncestors);
|
|
39
|
+
const includeDescendants = (session) => {
|
|
40
|
+
const queue = [...((session && session.childIds) || [])];
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
while (queue.length) {
|
|
43
|
+
const id = queue.shift();
|
|
44
|
+
if (!id || seen.has(id)) continue;
|
|
45
|
+
seen.add(id);
|
|
46
|
+
included.add(id);
|
|
47
|
+
const child = byId.get(id);
|
|
48
|
+
if (child) queue.push(...(child.childIds || []));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const requestedFocus = focusId && byId.get(focusId);
|
|
52
|
+
if (requestedFocus) {
|
|
53
|
+
includeAncestors(requestedFocus);
|
|
54
|
+
includeDescendants(requestedFocus);
|
|
55
|
+
}
|
|
56
|
+
for (const id of [...included]) {
|
|
57
|
+
const session = byId.get(id);
|
|
58
|
+
for (const childId of (session && session.childIds) || []) included.add(childId);
|
|
59
|
+
}
|
|
60
|
+
return { byId, included, nodes: sessions.filter((session) => included.has(session.id)) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sortGraphNodes(sessions) {
|
|
64
|
+
return [...sessions].sort((a, b) => {
|
|
65
|
+
const statusA = isLiveSession(a) ? 3 : a.status === "waiting" ? 2 : a.status === "failed" ? 1 : 0;
|
|
66
|
+
const statusB = isLiveSession(b) ? 3 : b.status === "waiting" ? 2 : b.status === "failed" ? 1 : 0;
|
|
67
|
+
return statusB - statusA || Date.parse(b.updatedAt || 0) - Date.parse(a.updatedAt || 0);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function graphChildren(session, model) {
|
|
72
|
+
return sortGraphNodes((session.childIds || []).map((id) => model.byId.get(id)).filter(Boolean));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function agentExecutionMode(session) {
|
|
76
|
+
const presence = Array.isArray(session && session.runtimePresence) ? session.runtimePresence : [];
|
|
77
|
+
const tmux = presence.find((item) => item.kind === "tmux");
|
|
78
|
+
if (tmux)
|
|
79
|
+
return {
|
|
80
|
+
kind: "tmux",
|
|
81
|
+
label: "TMUX 사용",
|
|
82
|
+
detail: [tmux.distro, tmux.sessionName, tmux.paneNativeId || tmux.paneId].filter(Boolean).join(" · ") || "분할 터미널에서 실행",
|
|
83
|
+
};
|
|
84
|
+
return { kind: "standard", label: "일반 실행", detail: "TMUX 미사용" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function executionModeBadge(session, compact = false) {
|
|
88
|
+
const mode = agentExecutionMode(session);
|
|
89
|
+
return `<span class="execution-mode-badge ${mode.kind}" title="${esc(mode.detail)}">
|
|
90
|
+
<i>${mode.kind === "tmux" ? "▦" : "›_"}</i>
|
|
91
|
+
<b>${esc(mode.label)}</b>${compact ? "" : `<small>${esc(mode.detail)}</small>`}</span>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function graphDescendantCount(session, model) {
|
|
95
|
+
const queue = [...(session.childIds || [])];
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
while (queue.length) {
|
|
98
|
+
const id = queue.shift();
|
|
99
|
+
if (!id || seen.has(id) || !model.byId.has(id)) continue;
|
|
100
|
+
seen.add(id);
|
|
101
|
+
queue.push(...(model.byId.get(id).childIds || []));
|
|
102
|
+
}
|
|
103
|
+
return seen.size;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { graphPath, connectedGraphSessions, sortGraphNodes, graphChildren, agentExecutionMode, executionModeBadge, graphDescendantCount };
|
|
107
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
4
|
+
|
|
5
|
+
window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOrchestration(context = {}) {
|
|
6
|
+
const {
|
|
7
|
+
$,
|
|
8
|
+
esc,
|
|
9
|
+
state,
|
|
10
|
+
readablePreview,
|
|
11
|
+
agentRoleLabel,
|
|
12
|
+
isLiveSession,
|
|
13
|
+
graphPath,
|
|
14
|
+
connectedGraphSessions,
|
|
15
|
+
sortGraphNodes,
|
|
16
|
+
agentExecutionMode,
|
|
17
|
+
liveTmuxEntries,
|
|
18
|
+
runtimeSeparatedOverview,
|
|
19
|
+
focusedGraph,
|
|
20
|
+
scheduleAgentWorkflowConnections,
|
|
21
|
+
} = context;
|
|
22
|
+
|
|
23
|
+
function renderAgentMap(sessions, motionKind = "refresh") {
|
|
24
|
+
const model = connectedGraphSessions(sessions);
|
|
25
|
+
const focus =
|
|
26
|
+
state.graphFocusId && model.byId.get(state.graphFocusId) && model.included.has(state.graphFocusId) ? model.byId.get(state.graphFocusId) : null;
|
|
27
|
+
if (state.graphFocusId && !focus) state.graphFocusId = null;
|
|
28
|
+
const roots = sortGraphNodes(model.nodes.filter((session) => !session.parentId || !model.included.has(session.parentId)));
|
|
29
|
+
if (!model.nodes.length) {
|
|
30
|
+
$("#liveSessionGrid").innerHTML = "";
|
|
31
|
+
$("#graphBreadcrumbs").innerHTML = "";
|
|
32
|
+
$("#graphResetBtn").classList.add("hidden");
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (focus) {
|
|
37
|
+
$("#liveSessionGrid").innerHTML = focusedGraph(focus, model, motionKind);
|
|
38
|
+
const path = graphPath(focus, model.byId);
|
|
39
|
+
$("#graphBreadcrumbs").innerHTML = `<button type="button" data-graph-reset>작업 목록</button>${path
|
|
40
|
+
.map((item) => {
|
|
41
|
+
const label = item.parentId ? item.agentName || agentRoleLabel(item.agentRole) : item.title;
|
|
42
|
+
const preview = readablePreview(label, item.parentId ? 42 : 72);
|
|
43
|
+
return `<i>›</i>
|
|
44
|
+
<button type="button" data-graph-focus="${esc(item.id)}"
|
|
45
|
+
class="${item.id === focus.id ? "current" : ""}"
|
|
46
|
+
title="${esc(preview.full)}">${esc(preview.text)}</button>`;
|
|
47
|
+
})
|
|
48
|
+
.join("")}`;
|
|
49
|
+
$("#graphResetBtn").classList.remove("hidden");
|
|
50
|
+
scheduleAgentWorkflowConnections();
|
|
51
|
+
} else {
|
|
52
|
+
const tmuxEntries = liveTmuxEntries();
|
|
53
|
+
const standardRoots = roots.filter((root) => agentExecutionMode(root).kind !== "tmux");
|
|
54
|
+
const activeCount = model.nodes.filter(isLiveSession).length + tmuxEntries.filter((entry) => !entry.agent.linkedSessionId).length;
|
|
55
|
+
$("#liveSessionGrid").innerHTML = `<details class="runtime-disclosure" open>
|
|
56
|
+
<summary>
|
|
57
|
+
<span>
|
|
58
|
+
<b>${activeCount}개 AI가 작업 중입니다</b>
|
|
59
|
+
<small>일반 실행과 tmux의 세부 흐름은 필요할 때 펼쳐보세요.</small>
|
|
60
|
+
</span>
|
|
61
|
+
<em>상세 흐름 보기 <i aria-hidden="true">↓</i>
|
|
62
|
+
</em>
|
|
63
|
+
</summary>${runtimeSeparatedOverview(roots, model)}</details>`;
|
|
64
|
+
$("#graphBreadcrumbs").innerHTML =
|
|
65
|
+
`<span class="map-hint">
|
|
66
|
+
일반 실행 <b>${standardRoots.length}</b>개 ·
|
|
67
|
+
TMUX AI <b>${tmuxEntries.length}</b>개 ·
|
|
68
|
+
<b>${model.nodes.filter((item) => item.parentId).length}</b>개 도움 AI
|
|
69
|
+
</span>`;
|
|
70
|
+
$("#graphResetBtn").classList.add("hidden");
|
|
71
|
+
}
|
|
72
|
+
return model.nodes.filter(isLiveSession).length + liveTmuxEntries().filter((entry) => !entry.agent.linkedSessionId).length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { renderAgentMap };
|
|
76
|
+
};
|