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.
Files changed (82) hide show
  1. package/README.ko.md +175 -0
  2. package/README.md +175 -0
  3. package/README.zh-CN.md +175 -0
  4. package/bin/loadtoagent.js +171 -0
  5. package/docs/ARCHITECTURE.md +30 -0
  6. package/docs/PROVIDER-CONTRACTS.md +58 -0
  7. package/docs/assets/loadtoagent-dashboard.png +0 -0
  8. package/docs/assets/loadtoagent-demo.gif +0 -0
  9. package/main.js +493 -0
  10. package/package.json +133 -0
  11. package/preload.js +75 -0
  12. package/renderer/app-agent-actions.js +285 -0
  13. package/renderer/app-bootstrap.js +95 -0
  14. package/renderer/app-dashboard.js +361 -0
  15. package/renderer/app-drawer-content.js +203 -0
  16. package/renderer/app-drawer-data.js +41 -0
  17. package/renderer/app-drawer.js +183 -0
  18. package/renderer/app-events-dialogs.js +169 -0
  19. package/renderer/app-events-filters.js +74 -0
  20. package/renderer/app-events-navigation.js +96 -0
  21. package/renderer/app-events-sessions.js +195 -0
  22. package/renderer/app-events.js +16 -0
  23. package/renderer/app-graph-layout.js +71 -0
  24. package/renderer/app-graph-model.js +107 -0
  25. package/renderer/app-graph-orchestration.js +76 -0
  26. package/renderer/app-graph-view.js +544 -0
  27. package/renderer/app-run-modal.js +221 -0
  28. package/renderer/app-session-render.js +243 -0
  29. package/renderer/app-tmux-render.js +247 -0
  30. package/renderer/app.js +608 -0
  31. package/renderer/fonts/geist-ext.woff2 +0 -0
  32. package/renderer/fonts/geist.woff2 +0 -0
  33. package/renderer/i18n-messages.js +355 -0
  34. package/renderer/i18n.js +122 -0
  35. package/renderer/index.html +386 -0
  36. package/renderer/shared.js +26 -0
  37. package/renderer/styles-agent-map.css +401 -0
  38. package/renderer/styles-cards.css +600 -0
  39. package/renderer/styles-collaboration.css +534 -0
  40. package/renderer/styles-components.css +1293 -0
  41. package/renderer/styles-onboarding.css +344 -0
  42. package/renderer/styles-overlays.css +284 -0
  43. package/renderer/styles-product.css +686 -0
  44. package/renderer/styles-responsive-product.css +689 -0
  45. package/renderer/styles-responsive-runtime.css +718 -0
  46. package/renderer/styles-responsive-shell.css +533 -0
  47. package/renderer/styles-responsive-workflows.css +778 -0
  48. package/renderer/styles-run-composer.css +695 -0
  49. package/renderer/styles-settings.css +606 -0
  50. package/renderer/styles-terminal.css +1241 -0
  51. package/renderer/styles-tmux.css +1162 -0
  52. package/renderer/styles-workflow-map.css +513 -0
  53. package/renderer/styles-workflows.css +1217 -0
  54. package/renderer/styles.css +486 -0
  55. package/renderer/terminal-agent.js +152 -0
  56. package/renderer/terminal-events.js +226 -0
  57. package/renderer/terminal-workbench.js +464 -0
  58. package/renderer/terminal.js +497 -0
  59. package/src/agentMonitor/claudeParser.js +197 -0
  60. package/src/agentMonitor/codexCollaboration.js +95 -0
  61. package/src/agentMonitor/codexParser.js +447 -0
  62. package/src/agentMonitor/genericParser.js +244 -0
  63. package/src/agentMonitor/hierarchy.js +248 -0
  64. package/src/agentMonitor/sessionFiles.js +86 -0
  65. package/src/agentMonitor.js +591 -0
  66. package/src/agentRunner.js +465 -0
  67. package/src/bridgeServer.js +246 -0
  68. package/src/contracts.js +71 -0
  69. package/src/diagnostics.js +23 -0
  70. package/src/ipc/registerAgentIpc.js +12 -0
  71. package/src/ipc/registerAppIpc.js +20 -0
  72. package/src/ipc/registerTerminalIpc.js +50 -0
  73. package/src/ipc/registerTmuxIpc.js +30 -0
  74. package/src/ipc/registerWorkspaceIpc.js +14 -0
  75. package/src/monitorWorker.js +273 -0
  76. package/src/processMonitor.js +394 -0
  77. package/src/providerRegistry.js +120 -0
  78. package/src/terminalManager.js +438 -0
  79. package/src/tmuxController.js +183 -0
  80. package/src/tmuxMonitor.js +432 -0
  81. package/src/updateManager.js +339 -0
  82. 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
+ };