loadtoagent 1.3.3 → 1.3.4
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/main.js +53 -8
- package/package.json +2 -1
- package/renderer/app-agent-actions.js +172 -66
- package/renderer/app-bootstrap.js +3 -1
- package/renderer/app-dashboard.js +36 -7
- package/renderer/app-drawer-content.js +177 -59
- package/renderer/app-drawer-data.js +7 -3
- package/renderer/app-drawer.js +72 -33
- package/renderer/app-events-dialogs.js +8 -1
- package/renderer/app-events-navigation.js +7 -0
- package/renderer/app-events-sessions.js +136 -13
- package/renderer/app-graph-model.js +2 -5
- package/renderer/app-graph-orchestration.js +7 -18
- package/renderer/app-graph-view.js +194 -48
- package/renderer/app-management.js +330 -35
- package/renderer/app-quality.js +5 -0
- package/renderer/app-session-render.js +20 -83
- package/renderer/app.js +5 -1
- package/renderer/i18n-messages.js +232 -21
- package/renderer/index.html +9 -6
- package/renderer/styles-components.css +140 -0
- package/renderer/styles-control-room.css +1439 -0
- package/renderer/styles-management.css +390 -3
- package/renderer/styles-responsive-shell.css +5 -0
- package/renderer/styles-runtime-overview.css +20 -0
- package/renderer/terminal-agent.js +31 -7
- package/renderer/terminal.js +4 -1
- package/src/agentMonitor/claudeParser.js +8 -4
- package/src/agentMonitor/codexParser.js +5 -4
- package/src/agentMonitor/genericParser.js +7 -6
- package/src/agentMonitor.js +44 -4
- package/src/attentionNotifier.js +3 -0
- package/src/ipc/registerTerminalIpc.js +2 -2
- package/src/monitorWorker.js +1 -1
- package/src/processMonitor.js +68 -7
- package/src/terminalManager.js +16 -2
|
@@ -4,13 +4,29 @@ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
|
|
|
4
4
|
|
|
5
5
|
window.LoadToAgentAppFactories.createSessionEventBindings = function createSessionEventBindings(context = {}) {
|
|
6
6
|
const {
|
|
7
|
-
$, state, selectView, renderProviderOverview, renderProviderFilter, toggleProviderFilter, announceProviderFilter, renderSessions, renderTmuxMap, openDrawer, openSubagentConversation,
|
|
8
|
-
dispatchAgentCommand, openAgentTerminal, copyBridgeCommand,
|
|
7
|
+
$, state, selectView, renderProviderOverview, renderProviderFilter, toggleProviderFilter, announceProviderFilter, renderSessions, renderTmuxMap, openDrawer, openSubagentConversation, openExecutionActivity,
|
|
8
|
+
dispatchAgentCommand, openAgentTerminal, copyBridgeCommand, saveDashboardPreferences = () => {},
|
|
9
9
|
controlManagedRun, quickRespond, prepareReassignment,
|
|
10
10
|
copyText = async () => false,
|
|
11
11
|
announce = () => {},
|
|
12
|
+
moveSessionOrder = () => false,
|
|
12
13
|
} = context;
|
|
13
14
|
|
|
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;
|
|
22
|
+
state.sort = "recent";
|
|
23
|
+
if ($("#sortSelect")) $("#sortSelect").value = "recent";
|
|
24
|
+
saveDashboardPreferences();
|
|
25
|
+
renderSessions("reorder");
|
|
26
|
+
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 }));
|
|
28
|
+
};
|
|
29
|
+
|
|
14
30
|
const managementFilterLabel = value => value === "all" ? window.LoadToAgentI18n.t("management.filter_all") : window.LoadToAgentI18n.t(`management.health.${value}`);
|
|
15
31
|
const announceManagementFilter = value => announce(window.LoadToAgentI18n.t("management.filter_results", {
|
|
16
32
|
filter: managementFilterLabel(value),
|
|
@@ -18,15 +34,85 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
18
34
|
}));
|
|
19
35
|
|
|
20
36
|
function bindManagementEvents() {
|
|
21
|
-
$("#operationsOverview").addEventListener("click", (event) => {
|
|
37
|
+
$("#operationsOverview").addEventListener("click", async (event) => {
|
|
38
|
+
const route = event.target.closest("[data-agent-command-route]");
|
|
39
|
+
if (route) {
|
|
40
|
+
state.agentCommandRoutes.set(route.dataset.agentCommandSession, route.dataset.agentCommandRoute);
|
|
41
|
+
renderSessions("route");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const intervention = event.target.closest("[data-supervision-intervention-open]");
|
|
45
|
+
if (intervention) {
|
|
46
|
+
const details = $("#operationsOverview")?.querySelector(`.supervision-intervention[data-disclosure-key="supervision:command:${CSS.escape(intervention.dataset.supervisionInterventionOpen)}"]`);
|
|
47
|
+
if (details) {
|
|
48
|
+
details.open = true;
|
|
49
|
+
details.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const supervision = event.target.closest("[data-supervision-focus]");
|
|
54
|
+
if (supervision) {
|
|
55
|
+
state.supervisionFocusId = supervision.dataset.supervisionFocus;
|
|
56
|
+
renderSessions("focus");
|
|
57
|
+
requestAnimationFrame(() => $("#operationsOverview")?.querySelector(`[data-supervision-focus="${CSS.escape(state.supervisionFocusId)}"]`)?.focus({ preventScroll: true }));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const graph = event.target.closest("[data-graph-focus]");
|
|
61
|
+
if (graph) {
|
|
62
|
+
state.graphFocusId = graph.dataset.graphFocus;
|
|
63
|
+
renderSessions("focus");
|
|
64
|
+
requestAnimationFrame(() => $("#liveSection")?.scrollIntoView({ behavior: "smooth", block: "start" }));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
22
67
|
const open = event.target.closest("[data-open-session]");
|
|
23
|
-
if (open)
|
|
68
|
+
if (open) {
|
|
69
|
+
const session = (state.snapshot?.sessions || []).find(item => item.id === open.dataset.openSession);
|
|
70
|
+
return session?.parentId ? openSubagentConversation(session.id) : openDrawer(open.dataset.openSession);
|
|
71
|
+
}
|
|
72
|
+
const bridge = event.target.closest("[data-agent-bridge-copy]");
|
|
73
|
+
if (bridge) return copyBridgeCommand(bridge.dataset.agentBridgeCopy);
|
|
74
|
+
const terminal = event.target.closest("[data-agent-terminal-open]");
|
|
75
|
+
if (terminal) return openAgentTerminal(terminal.dataset.agentTerminalOpen);
|
|
76
|
+
const managed = event.target.closest("[data-managed-run-action]");
|
|
77
|
+
if (managed) return controlManagedRun(managed.dataset.managementSessionId, managed.dataset.managedRunAction);
|
|
78
|
+
const reassign = event.target.closest("[data-reassign-session]");
|
|
79
|
+
if (reassign) return prepareReassignment(reassign.dataset.reassignSession);
|
|
24
80
|
const filter = event.target.closest("[data-management-filter]");
|
|
25
81
|
if (!filter) return;
|
|
26
82
|
selectView("waiting", { focusMain: true, managementFilter: filter.dataset.managementFilter });
|
|
27
83
|
announceManagementFilter(filter.dataset.managementFilter);
|
|
28
84
|
});
|
|
85
|
+
$("#operationsOverview").addEventListener("input", (event) => {
|
|
86
|
+
const input = event.target.closest("[data-agent-command-draft]");
|
|
87
|
+
if (input) state.agentCommandDrafts.set(input.dataset.agentCommandDraft, input.value);
|
|
88
|
+
});
|
|
89
|
+
$("#operationsOverview").addEventListener("change", (event) => {
|
|
90
|
+
const picker = event.target.closest("[data-agent-command-target]");
|
|
91
|
+
if (!picker) return;
|
|
92
|
+
if (picker.value) state.agentCommandTargets.set(picker.dataset.agentCommandTarget, picker.value);
|
|
93
|
+
else state.agentCommandTargets.delete(picker.dataset.agentCommandTarget);
|
|
94
|
+
const enabled = Boolean(picker.value);
|
|
95
|
+
picker.closest("form")?.querySelectorAll("[data-agent-terminal-open], button[type='submit']").forEach(button => { button.disabled = !enabled; });
|
|
96
|
+
});
|
|
97
|
+
$("#operationsOverview").addEventListener("keydown", (event) => {
|
|
98
|
+
const input = event.target.closest("[data-agent-command-draft]");
|
|
99
|
+
if (!input || event.key !== "Enter" || event.shiftKey || event.isComposing || event.keyCode === 229) return;
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
input.closest("form")?.requestSubmit();
|
|
102
|
+
});
|
|
103
|
+
$("#operationsOverview").addEventListener("submit", (event) => {
|
|
104
|
+
const form = event.target.closest("[data-agent-command-form]");
|
|
105
|
+
if (!form) return;
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
dispatchAgentCommand(form.dataset.agentCommandForm, form);
|
|
108
|
+
});
|
|
29
109
|
$("#attentionInbox").addEventListener("click", async (event) => {
|
|
110
|
+
const route = event.target.closest("[data-agent-command-route]");
|
|
111
|
+
if (route) {
|
|
112
|
+
state.agentCommandRoutes.set(route.dataset.agentCommandSession, route.dataset.agentCommandRoute);
|
|
113
|
+
renderSessions("route");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
30
116
|
const filter = event.target.closest("[data-management-inbox-filter]");
|
|
31
117
|
if (filter) {
|
|
32
118
|
state.managementFilter = filter.dataset.managementInboxFilter;
|
|
@@ -35,8 +121,24 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
35
121
|
requestAnimationFrame(() => $("#attentionInbox")?.querySelector(`[data-management-inbox-filter="${CSS.escape(state.managementFilter)}"]`)?.focus({ preventScroll: true }));
|
|
36
122
|
return;
|
|
37
123
|
}
|
|
124
|
+
const draft = event.target.closest("[data-attention-draft]");
|
|
125
|
+
if (draft) {
|
|
126
|
+
const sessionId = draft.dataset.attentionSessionId;
|
|
127
|
+
const value = draft.dataset.attentionDraft || "";
|
|
128
|
+
state.agentCommandDrafts.set(sessionId, value);
|
|
129
|
+
const input = $("#attentionInbox")?.querySelector(`[data-agent-command-draft="${CSS.escape(sessionId)}"]`);
|
|
130
|
+
if (input) {
|
|
131
|
+
input.value = value;
|
|
132
|
+
input.focus({ preventScroll: true });
|
|
133
|
+
input.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
38
137
|
const open = event.target.closest("[data-open-session]");
|
|
39
|
-
if (open)
|
|
138
|
+
if (open) {
|
|
139
|
+
const session = (state.snapshot?.sessions || []).find(item => item.id === open.dataset.openSession);
|
|
140
|
+
return session?.parentId ? openSubagentConversation(session.id) : openDrawer(open.dataset.openSession);
|
|
141
|
+
}
|
|
40
142
|
const quick = event.target.closest("[data-attention-quick]");
|
|
41
143
|
if (quick) return quickRespond(quick.dataset.attentionSessionId, quick.dataset.attentionQuick, $("#attentionInbox"));
|
|
42
144
|
const managed = event.target.closest("[data-managed-run-action]");
|
|
@@ -53,7 +155,7 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
53
155
|
if (!picker) return;
|
|
54
156
|
if (picker.value) state.agentCommandTargets.set(picker.dataset.agentCommandTarget, picker.value);
|
|
55
157
|
else state.agentCommandTargets.delete(picker.dataset.agentCommandTarget);
|
|
56
|
-
picker.closest("form")?.querySelectorAll("button").forEach(button => { button.disabled = !picker.value; });
|
|
158
|
+
picker.closest("form")?.querySelectorAll("[data-agent-terminal-open], button[type='submit']").forEach(button => { button.disabled = !picker.value; });
|
|
57
159
|
});
|
|
58
160
|
$("#attentionInbox").addEventListener("keydown", (event) => {
|
|
59
161
|
const input = event.target.closest("[data-agent-command-draft]");
|
|
@@ -126,10 +228,17 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
126
228
|
cards[next].focus();
|
|
127
229
|
});
|
|
128
230
|
$("#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
|
+
}
|
|
129
237
|
const card = event.target.closest("[data-session-id]");
|
|
130
238
|
if (card) openDrawer(card.dataset.sessionId);
|
|
131
239
|
});
|
|
132
240
|
$("#sessionGrid").addEventListener("keydown", (event) => {
|
|
241
|
+
if (event.target.closest("button, input, select, textarea, [contenteditable='true']")) return;
|
|
133
242
|
const card = event.target.closest("[data-session-id]");
|
|
134
243
|
if (card && (event.key === "Enter" || event.key === " ")) {
|
|
135
244
|
event.preventDefault();
|
|
@@ -140,6 +249,20 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
140
249
|
|
|
141
250
|
function bindLiveAgentEvents() {
|
|
142
251
|
$("#liveSessionGrid").addEventListener("click", async (event) => {
|
|
252
|
+
const order = event.target.closest("[data-session-order-move]");
|
|
253
|
+
if (order) {
|
|
254
|
+
event.stopPropagation();
|
|
255
|
+
moveVisibleSession(order, $("#liveSessionGrid"), "[data-control-session]");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const route = event.target.closest("[data-agent-command-route]");
|
|
259
|
+
if (route) {
|
|
260
|
+
event.stopPropagation();
|
|
261
|
+
state.agentCommandRoutes.set(route.dataset.agentCommandSession, route.dataset.agentCommandRoute);
|
|
262
|
+
renderSessions("route");
|
|
263
|
+
requestAnimationFrame(() => $("#liveSessionGrid")?.querySelector(`[data-agent-command-session="${CSS.escape(route.dataset.agentCommandSession)}"][data-agent-command-route="${CSS.escape(route.dataset.agentCommandRoute)}"]`)?.focus({ preventScroll: true }));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
143
266
|
const copy = event.target.closest("[data-copy-text]");
|
|
144
267
|
if (copy) {
|
|
145
268
|
event.stopPropagation();
|
|
@@ -171,12 +294,6 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
171
294
|
copyBridgeCommand(bridge.dataset.agentBridgeCopy);
|
|
172
295
|
return;
|
|
173
296
|
}
|
|
174
|
-
const origin = event.target.closest("[data-agent-open-origin]");
|
|
175
|
-
if (origin) {
|
|
176
|
-
event.stopPropagation();
|
|
177
|
-
openSessionOrigin(origin.dataset.agentOpenOrigin);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
297
|
const terminal = event.target.closest("[data-agent-terminal-open]");
|
|
181
298
|
if (terminal) {
|
|
182
299
|
event.stopPropagation();
|
|
@@ -203,6 +320,12 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
203
320
|
renderSessions("expand");
|
|
204
321
|
return;
|
|
205
322
|
}
|
|
323
|
+
const execution = event.target.closest("[data-open-execution-id]");
|
|
324
|
+
if (execution) {
|
|
325
|
+
event.stopPropagation();
|
|
326
|
+
openExecutionActivity(execution.dataset.openExecutionOwner, execution.dataset.openExecutionId);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
206
329
|
const open = event.target.closest("[data-open-session]");
|
|
207
330
|
if (open) {
|
|
208
331
|
event.stopPropagation();
|
|
@@ -235,7 +358,7 @@ window.LoadToAgentAppFactories.createSessionEventBindings = function createSessi
|
|
|
235
358
|
const form = picker.closest("[data-agent-command-form]");
|
|
236
359
|
const enabled = Boolean(picker.value);
|
|
237
360
|
form &&
|
|
238
|
-
form.querySelectorAll("button").forEach((button) => {
|
|
361
|
+
form.querySelectorAll("[data-agent-terminal-open], button[type='submit']").forEach((button) => {
|
|
239
362
|
button.disabled = !enabled;
|
|
240
363
|
});
|
|
241
364
|
});
|
|
@@ -10,6 +10,7 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
|
|
|
10
10
|
state,
|
|
11
11
|
compact,
|
|
12
12
|
isLiveSession,
|
|
13
|
+
stableSessionSort = sessions => [...sessions],
|
|
13
14
|
} = context;
|
|
14
15
|
|
|
15
16
|
function graphPath(session, byId) {
|
|
@@ -62,11 +63,7 @@ window.LoadToAgentAppFactories.createGraphModel = function createGraphModel(cont
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
function sortGraphNodes(sessions) {
|
|
65
|
-
return
|
|
66
|
-
const statusA = isLiveSession(a) ? 3 : a.status === "waiting" ? 2 : a.status === "failed" ? 1 : 0;
|
|
67
|
-
const statusB = isLiveSession(b) ? 3 : b.status === "waiting" ? 2 : b.status === "failed" ? 1 : 0;
|
|
68
|
-
return statusB - statusA || Date.parse(b.updatedAt || 0) - Date.parse(a.updatedAt || 0);
|
|
69
|
-
});
|
|
66
|
+
return stableSessionSort(sessions);
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
function graphChildren(session, model) {
|
|
@@ -51,24 +51,13 @@ window.LoadToAgentAppFactories.createGraphOrchestration = function createGraphOr
|
|
|
51
51
|
scheduleAgentWorkflowConnections();
|
|
52
52
|
} else {
|
|
53
53
|
const runtime = runtimeAgentSummary(model, liveTmuxEntries(state.snapshot && state.snapshot.tmux));
|
|
54
|
-
$("#liveSessionGrid").innerHTML =
|
|
55
|
-
|
|
56
|
-
<span>
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
</
|
|
60
|
-
|
|
61
|
-
</em>
|
|
62
|
-
</summary>${runtimeSeparatedOverview(roots, model)}</details>`;
|
|
63
|
-
$("#graphBreadcrumbs").innerHTML =
|
|
64
|
-
`<span class="map-hint">
|
|
65
|
-
${esc(t("graph.standard_ai"))} <b>${runtime.standardCount}</b> ·
|
|
66
|
-
${esc(t("graph.tmux_ai"))} <b>${runtime.tmuxCount}</b> ·
|
|
67
|
-
${esc(t("graph.active_helper_ai"))} <b>${runtime.activeHelperCount}</b> ·
|
|
68
|
-
${esc(t("graph.helper_ai_history"))} <b>${runtime.helperRecordCount}</b> ·
|
|
69
|
-
${esc(t("graph.running_shell"))} <b>${runtime.shellExecutionCount}</b> ·
|
|
70
|
-
${esc(t("graph.running_background"))} <b>${runtime.backgroundExecutionCount}</b>
|
|
71
|
-
</span>`;
|
|
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>`;
|
|
72
61
|
$("#graphResetBtn").classList.add("hidden");
|
|
73
62
|
return runtime.activeCount;
|
|
74
63
|
}
|
|
@@ -288,55 +288,200 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
|
|
|
288
288
|
</article>`;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
function controlRoomDescendants(root, model) {
|
|
292
|
+
const found = [];
|
|
293
|
+
const queue = [...(root.childIds || [])];
|
|
294
|
+
const visited = new Set();
|
|
295
|
+
while (queue.length) {
|
|
296
|
+
const id = queue.shift();
|
|
297
|
+
if (!id || visited.has(id)) continue;
|
|
298
|
+
visited.add(id);
|
|
299
|
+
const child = model.byId.get(id);
|
|
300
|
+
if (!child) continue;
|
|
301
|
+
found.push(child);
|
|
302
|
+
queue.push(...(child.childIds || []));
|
|
303
|
+
}
|
|
304
|
+
return sortGraphNodes(found);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function controlRoomIntent(value) {
|
|
308
|
+
const text = String(value || "");
|
|
309
|
+
const loopCommand = ["w", "c", "c", "-loop"].join("");
|
|
310
|
+
const loop = text.match(new RegExp(`^/${loopCommand}\\s+--tick\\s+([^\\s]+)`, "i"));
|
|
311
|
+
if (loop) return t("control.summary_automatic_run", { name: loop[1] });
|
|
312
|
+
const rules = [
|
|
313
|
+
[/(?:서브\s*에이전트|helper|subagent).*(?:직접|direct).*(?:전달|메시지|message|개입)|(?:직접|direct).*(?:서브\s*에이전트|helper|subagent)/i, "control.summary_direct_helper"],
|
|
314
|
+
[/(?:메인|lead).*(?:서브\s*에이전트|helper|subagent).*(?:문구|요약|설명|summary)|(?:에이전트|agent).*(?:백그라운드|실행\s*작업|execution).*(?:문구|요약|설명|summary)/i, "control.summary_agent_work_copy"],
|
|
315
|
+
[/(?:전체|모든).*(?:대화|기록|메시지).*(?:생략|전체|불러|표시)|(?:full|entire).*(?:conversation|history|messages)/i, "control.summary_full_history"],
|
|
316
|
+
[/(?:관제|control\s*room|실행\s*구조|홈\s*화면).*(?:에이전트|agent|세션|session|흐름|flow)/i, "control.summary_control_room"],
|
|
317
|
+
[/(?:requirements?|요구사항).*(?:phase|단계).*(?:complete|완료|조건|contract)|(?:phase|단계).*(?:complete|완료).*(?:requirements?|요구사항)/i, "control.summary_phase_requirements"],
|
|
318
|
+
[/(?:build|package|빌드|패키징).*(?:restart|relaunch|다시\s*(?:실행|켜)|재실행)|(?:종료|stop).*(?:빌드|build).*(?:실행|start)/i, "control.summary_build_restart"],
|
|
319
|
+
[/(?:agentMonitor|snapshot\.sessions|세션\s*데이터).*(?:summary|요약|입력|확인|inspect)/i, "control.summary_session_data"],
|
|
320
|
+
[/(?:test|tests|testing|pytest|jest|vitest|테스트|회귀\s*검증).*(?:result|verify|pass|결과|확인|검증)|(?:검증|verify).*(?:기능|동작|화면|UI)/i, "control.work_test"],
|
|
321
|
+
[/(?:UI|화면|카드|문구|표시).*(?:가독성|읽기|요약|축약|정리|개선)/i, "control.summary_readability"],
|
|
322
|
+
[/(?:bug|error|failure|오류|버그|실패).*(?:fix|resolve|수정|해결|원인)/i, "control.summary_fix_problem"],
|
|
323
|
+
[/(?:review|audit|검토|감사).*(?:code|UI|화면|구조|변경|코드)/i, "control.summary_review"],
|
|
324
|
+
[/(?:read|inspect|analy[sz]e|search|확인|조사|분석|검색).*(?:file|code|config|log|파일|코드|설정|로그)/i, "control.work_inspect_code"],
|
|
325
|
+
];
|
|
326
|
+
const matched = rules.find(([pattern]) => pattern.test(text));
|
|
327
|
+
return matched ? t(matched[1]) : text;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function controlRoomSummary(value, maxCharacters = 64) {
|
|
331
|
+
const cleaned = String(value || "")
|
|
332
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
333
|
+
.replace(/<[^>]+>/g, " ")
|
|
334
|
+
.replace(/^[\s>*#\-\d.)]+/, "")
|
|
335
|
+
.replace(/\*\*|__|`/g, "")
|
|
336
|
+
.replace(/^(?:now\s+)?(?:i(?:'ll|\s+will)|let\s+me|next,?)\s+/i, "")
|
|
337
|
+
.replace(/\s+/g, " ")
|
|
338
|
+
.trim();
|
|
339
|
+
if (!cleaned) return readablePreview(t("control.work_unknown"), maxCharacters);
|
|
340
|
+
const intent = controlRoomIntent(cleaned);
|
|
341
|
+
const sentence = intent === cleaned ? cleaned.match(/^(.{12,}?[.!?。!?])(?:\s|$)/)?.[1] || cleaned : intent;
|
|
342
|
+
return readablePreview(sentence, maxCharacters);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function controlRoomAgentGoal(session, maxCharacters = 64) {
|
|
346
|
+
const delegation = session.delegation || {};
|
|
347
|
+
const messages = session.messages || [];
|
|
348
|
+
const userGoal = [...messages].reverse().find(message => message.role === "user" && message.text && !/^\s*\/[\w-]+(?:\s|$)/.test(message.text));
|
|
349
|
+
const title = String(session.title || "");
|
|
350
|
+
const source = delegation.assignment || session.sharedGoal || title
|
|
351
|
+
|| userGoal?.text || delegation.taskName || session.taskName || latestWorkCopy(session);
|
|
352
|
+
return controlRoomSummary(source, maxCharacters);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function looksLikeExecutionCommand(value) {
|
|
356
|
+
const text = String(value || "").trim();
|
|
357
|
+
return /(?:^|[;&|]\s*)(?:npm|pnpm|yarn|bun|npx|node|git|rg|grep|findstr|python|pytest|gradle|mvn|docker|curl|pwsh|powershell|cmd|start-process|get-content|select-string)\b/i.test(text)
|
|
358
|
+
|| /(?:--[\w-]+|\\[^\s]+|\/[\w.-]+\.[a-z0-9]{1,8})/i.test(text);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function inferredExecutionSummary(activity) {
|
|
362
|
+
const command = String(activity.command || "").replace(/\s+/g, " ").trim();
|
|
363
|
+
const searchable = `${activity.description || ""} ${activity.label || ""} ${command}`.toLowerCase();
|
|
364
|
+
const patterns = [
|
|
365
|
+
[/(?:electron-builder|\bdist(?::win)?\b|\bpackage\b|\bportable\b|\bnsis\b)/, "control.work_package_app"],
|
|
366
|
+
[/(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve)\b|\bdev\s+server\b|개발\s*서버/, "control.work_dev_server"],
|
|
367
|
+
[/\b(?:test|tests|pytest|jest|vitest|playwright|cypress)\b|테스트|회귀\s*검증/, "control.work_test"],
|
|
368
|
+
[/\b(?:build|compile|tsc)\b|빌드|컴파일/, "control.work_build"],
|
|
369
|
+
[/\b(?:install|npm\s+ci|pnpm\s+i)\b|패키지\s*설치|의존성/, "control.work_install"],
|
|
370
|
+
[/\b(?:lint|eslint|stylelint)\b|정적\s*검사/, "control.work_lint"],
|
|
371
|
+
[/\b(?:format|prettier)\b|코드\s*정리/, "control.work_format"],
|
|
372
|
+
[/\bgit\s+(?:status|diff|show|log)\b|변경\s*(?:내용|사항).*확인/, "control.work_review_changes"],
|
|
373
|
+
[/\bgit\s+commit\b|커밋/, "control.work_save_changes"],
|
|
374
|
+
[/\bgit\s+push\b|업로드|배포/, "control.work_publish"],
|
|
375
|
+
[/(?:\brg\b|grep|findstr|select-string|get-content|read-file|코드.*확인|파일.*확인)/, "control.work_inspect_code"],
|
|
376
|
+
[/(?:start-process|loadtoagent\.exe|electron\s+\.)|프로그램\s*실행|앱\s*실행/, "control.work_launch_app"],
|
|
377
|
+
[/(?:index|인덱스).*(?:refresh|update|갱신)/, "control.work_refresh_index"],
|
|
378
|
+
[/(?:crawl|scrape|수집)/, "control.work_collect_data"],
|
|
379
|
+
];
|
|
380
|
+
const matched = patterns.find(([pattern]) => pattern.test(searchable));
|
|
381
|
+
if (matched) return controlRoomSummary(t(matched[1]), 58);
|
|
382
|
+
const label = String(activity.description || activity.label || "").trim();
|
|
383
|
+
const generic = /^(?:shell|셸\s*명령|background|백그라운드\s*(?:작업|명령|실행)|foreground|포그라운드\s*(?:작업|명령|실행)|일반\s*명령\s*실행)$/i.test(label);
|
|
384
|
+
if (label && !generic && !looksLikeExecutionCommand(label)) return controlRoomSummary(label, 58);
|
|
385
|
+
if (command) return controlRoomSummary(command.replace(/^[^\s]+\s+/, ""), 58);
|
|
386
|
+
return controlRoomSummary(t("control.work_background"), 58);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function controlRoomChildNode(child) {
|
|
390
|
+
const provider = providerInfo(child.provider);
|
|
391
|
+
const delegation = child.delegation || {};
|
|
392
|
+
const assignment = delegation.assignment || delegation.taskName || child.taskName || child.title;
|
|
393
|
+
const current = controlRoomSummary(latestWorkCopy(child) || child.statusDetail || assignment, 72);
|
|
394
|
+
const assignmentPreview = controlRoomAgentGoal(child, 58);
|
|
395
|
+
const workState = subagentWorkState(child);
|
|
396
|
+
return `<button type="button" class="control-room-node helper-node is-${esc(workState)} ${child.status === "starting" ? "is-spawning" : ""}"
|
|
397
|
+
data-open-subagent-chat="${esc(child.id)}"
|
|
398
|
+
data-control-summary="${esc(assignmentPreview.text)}"
|
|
399
|
+
data-motion-key="control-helper:${esc(child.id)}"
|
|
400
|
+
data-motion-value="${esc(child.status || "")}:${esc(child.updatedAt || "")}"
|
|
401
|
+
style="${providerStyle(child.provider)}" aria-label="${esc(t("control.open_subagent", { task: assignment }))}">
|
|
402
|
+
<span class="control-node-icon">${esc(provider.mark)}</span>
|
|
403
|
+
<span class="control-node-copy"><small>${esc(t("control.subagent_work"))} · ${esc(provider.label)}</small><b title="${esc(assignmentPreview.full)}">${esc(assignmentPreview.text)}</b><em title="${esc(current.full)}">${esc(t("control.current_summary", { summary: current.text }))}</em></span>
|
|
404
|
+
<span class="control-node-state"><i aria-hidden="true"></i><b>${esc(subagentWorkLabel(child))}</b></span>
|
|
405
|
+
</button>`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function controlRoomExecutionNode(item) {
|
|
409
|
+
const { activity, owner } = item;
|
|
410
|
+
const summary = inferredExecutionSummary(activity);
|
|
411
|
+
const command = controlRoomSummary(activity.command || activity.description || activity.label || summary.full, 76);
|
|
412
|
+
const runtime = activity.kind === "shell"
|
|
413
|
+
? activity.runtime || state.platform?.localShellLabel || activity.tool || "PowerShell"
|
|
414
|
+
: activity.runtime || activity.tool || t("graph.background_task");
|
|
415
|
+
const executionKind = activity.mode === "background" || activity.status === "running"
|
|
416
|
+
? t("control.background_work_kind")
|
|
417
|
+
: t("control.command_work_kind");
|
|
418
|
+
const executionLabel = t("control.runtime_work", { runtime, kind: executionKind });
|
|
419
|
+
const ownerGoal = controlRoomAgentGoal(owner, 52);
|
|
420
|
+
const running = activity.status === "running";
|
|
421
|
+
return `<button type="button" class="control-room-node execution-node ${running ? "is-running" : "is-complete"}"
|
|
422
|
+
data-open-execution-owner="${esc(owner.id)}"
|
|
423
|
+
data-open-execution-id="${esc(activity.id)}"
|
|
424
|
+
data-control-summary="${esc(summary.text)}"
|
|
425
|
+
data-motion-key="control-execution:${esc(activity.id)}"
|
|
426
|
+
data-motion-value="${esc(activity.status || "")}:${esc(activity.updatedAt || activity.startedAt || "")}"
|
|
427
|
+
aria-label="${esc(t("control.open_execution_detail", { task: summary.text }))}">
|
|
428
|
+
<span class="control-node-icon">${activity.kind === "shell" ? "›_" : "◌"}</span>
|
|
429
|
+
<span class="control-node-copy"><small title="${esc(runtime)}">${esc(executionLabel)}</small><b title="${esc(summary.full)}">${esc(summary.text)}</b><em title="${esc(command.full)}">${esc(t("control.execution_context", { task: ownerGoal.text }))}</em></span>
|
|
430
|
+
<span class="control-node-state"><i aria-hidden="true"></i><b>${esc(executionActivityStatus(activity))}</b></span>
|
|
431
|
+
</button>`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function controlRoomSession(root, model) {
|
|
435
|
+
const provider = providerInfo(root.provider);
|
|
436
|
+
const descendants = controlRoomDescendants(root, model);
|
|
437
|
+
const actors = [root, ...descendants];
|
|
438
|
+
const executionItems = actors.flatMap(owner => (owner.executions || []).map(activity => ({ activity, owner })));
|
|
439
|
+
const activeChildren = descendants.filter(child => !["completed", "cancelled", "failed"].includes(child.status) && !child.completionObserved);
|
|
440
|
+
const completedChildren = descendants.filter(child => ["completed", "cancelled", "failed"].includes(child.status) || child.completionObserved);
|
|
441
|
+
const activeExecutions = executionItems.filter(item => item.activity.status === "running");
|
|
442
|
+
const completedExecutions = executionItems
|
|
443
|
+
.filter(item => item.activity.status !== "running")
|
|
444
|
+
.sort((a, b) => Date.parse(b.activity.updatedAt || b.activity.startedAt || 0) - Date.parse(a.activity.updatedAt || a.activity.startedAt || 0));
|
|
445
|
+
const activeUnits = [...activeChildren.map(child => ({ kind: "child", child })), ...activeExecutions.map(item => ({ kind: "execution", item }))];
|
|
446
|
+
const completedUnits = [
|
|
447
|
+
...completedChildren.map(child => ({ kind: "child", child, timestamp: child.completedAt || child.updatedAt })),
|
|
448
|
+
...completedExecutions.map(item => ({ kind: "execution", item, timestamp: item.activity.updatedAt || item.activity.startedAt })),
|
|
449
|
+
].sort((a, b) => Date.parse(b.timestamp || 0) - Date.parse(a.timestamp || 0)).slice(0, 3);
|
|
450
|
+
const current = controlRoomSummary(latestWorkCopy(root) || root.statusDetail || root.title, 74);
|
|
451
|
+
const title = controlRoomAgentGoal(root, 64);
|
|
452
|
+
const unitCount = activeUnits.length;
|
|
453
|
+
const main = `<button type="button" class="control-room-main" data-open-session="${esc(root.id)}"
|
|
454
|
+
data-control-summary="${esc(title.text)}"
|
|
455
|
+
data-motion-key="control-main:${esc(root.id)}" data-motion-value="${esc(root.updatedAt || "")}:${esc(root.status || "")}"
|
|
456
|
+
style="${providerStyle(root.provider)}">
|
|
457
|
+
<span class="control-main-top"><span class="provider-mark">${esc(provider.mark)}</span><span><small>${esc(t("control.main_agent"))}</small><b>${esc(provider.label)} · ${esc(root.model || t("session.model_unknown"))}</b></span><em><i aria-hidden="true"></i>${esc(statusLabel(root.status))}</em></span>
|
|
458
|
+
<strong title="${esc(title.full)}">${esc(title.text)}</strong>
|
|
459
|
+
<span class="control-main-now"><small>${esc(t("graph.current_work"))}</small><b title="${esc(current.full)}">${esc(current.text)}</b></span>
|
|
460
|
+
<span class="control-main-meta"><small>${esc(t("control.unit_counts", { helpers: activeChildren.length, executions: activeExecutions.length }))}</small><b>${esc(t("graph.view_conversation"))} →</b></span>
|
|
461
|
+
</button>`;
|
|
462
|
+
const shownActiveUnits = activeUnits.slice(0, 6);
|
|
463
|
+
const hiddenActiveUnits = Math.max(0, activeUnits.length - shownActiveUnits.length);
|
|
464
|
+
const active = activeUnits.length
|
|
465
|
+
? `${shownActiveUnits.map(unit => unit.kind === "child" ? controlRoomChildNode(unit.child) : controlRoomExecutionNode(unit.item)).join("")}${hiddenActiveUnits ? `<button type="button" class="control-room-node overflow-node" data-graph-focus="${esc(root.id)}"><span class="control-node-icon">+${hiddenActiveUnits}</span><span class="control-node-copy"><small>${esc(t("control.more_live_units"))}</small><b>${esc(t("control.open_remaining_units", { count: hiddenActiveUnits }))}</b><em>${esc(t("control.open_full_flow"))}</em></span><span class="control-node-state"><b>→</b></span></button>` : ""}`
|
|
466
|
+
: `<div class="control-room-running-empty"><span>○</span><small>${esc(t("control.running_empty"))}</small></div>`;
|
|
467
|
+
const completed = completedUnits.length
|
|
468
|
+
? completedUnits.map(unit => unit.kind === "child" ? controlRoomChildNode(unit.child) : controlRoomExecutionNode(unit.item)).join("")
|
|
469
|
+
: `<div class="control-room-complete-empty"><span>✓</span><small>${esc(t("control.completed_empty"))}</small></div>`;
|
|
470
|
+
return `<article class="control-room-session" data-control-session="${esc(root.id)}" style="${providerStyle(root.provider)}">
|
|
471
|
+
<header><div><span class="control-session-live"><i></i>${esc(t("control.live_session"))}</span><b>${esc(title.text)}</b></div><span class="session-order-actions control-session-order" role="group" aria-label="${esc(t("session.change_position"))}"><button type="button" data-session-order-move="${esc(root.id)}" data-session-order-offset="-1" title="${esc(t("session.move_up"))}" aria-label="${esc(t("session.move_up"))}">↑</button><button type="button" data-session-order-move="${esc(root.id)}" data-session-order-offset="1" title="${esc(t("session.move_down"))}" aria-label="${esc(t("session.move_down"))}">↓</button></span><button type="button" data-graph-focus="${esc(root.id)}">${esc(t("control.open_full_flow"))} ↗</button></header>
|
|
472
|
+
<div class="control-room-flow">
|
|
473
|
+
<section class="control-room-column main-column"><span class="control-column-label">${esc(t("control.main_work_column"))}</span>${main}</section>
|
|
474
|
+
<span class="control-flow-link live" aria-hidden="true"><i></i></span>
|
|
475
|
+
<section class="control-room-column activity-column"><span class="control-column-label">${esc(t("control.running_work_column"))}<b>${unitCount}</b></span><div class="control-room-node-list">${active}</div></section>
|
|
476
|
+
<span class="control-flow-link complete" aria-hidden="true"><i></i></span>
|
|
477
|
+
<section class="control-room-column completed-column"><span class="control-column-label">${esc(t("control.completed_work_column"))}<b>${completedUnits.length}</b></span><div class="control-room-node-list completed-list">${completed}</div></section>
|
|
478
|
+
</div>
|
|
479
|
+
</article>`;
|
|
480
|
+
}
|
|
481
|
+
|
|
291
482
|
function runtimeSeparatedOverview(roots, model) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const workflowUsesTmux = root => {
|
|
295
|
-
const queue = [...(root.childIds || [])];
|
|
296
|
-
const visited = new Set();
|
|
297
|
-
while (queue.length) {
|
|
298
|
-
const id = queue.shift();
|
|
299
|
-
if (!id || visited.has(id)) continue;
|
|
300
|
-
visited.add(id);
|
|
301
|
-
const child = model.byId.get(id);
|
|
302
|
-
if (!child) continue;
|
|
303
|
-
if (agentExecutionMode(child).kind === "tmux" || tmuxLinkedIds.has(child.id)) return true;
|
|
304
|
-
queue.push(...(child.childIds || []));
|
|
305
|
-
}
|
|
306
|
-
return false;
|
|
307
|
-
};
|
|
308
|
-
const tmuxRoots = roots.filter((root) => agentExecutionMode(root).kind === "tmux" || tmuxLinkedIds.has(root.id) || workflowUsesTmux(root));
|
|
309
|
-
const tmuxRootIds = new Set(tmuxRoots.map((root) => root.id));
|
|
310
|
-
const standardRoots = roots.filter((root) => !tmuxRootIds.has(root.id));
|
|
311
|
-
const providerOrder = [...new Set([...visibleProviders().map((item) => item.id), ...roots.map((item) => item.provider).filter(isProviderVisible)])];
|
|
312
|
-
const lanesFor = (items) =>
|
|
313
|
-
providerOrder.map((providerId) => ({ providerId, roots: items.filter((root) => root.provider === providerId) })).filter((item) => item.roots.length);
|
|
314
|
-
const standardLanes = lanesFor(standardRoots);
|
|
315
|
-
const tmuxLanes = lanesFor(tmuxRoots);
|
|
316
|
-
const standardHtml = standardLanes.length
|
|
317
|
-
? `<div class="agent-flow-overview">${standardLanes.map((item) => providerFlowLane(item.providerId, item.roots, model)).join("")}</div>`
|
|
318
|
-
: `<div class="runtime-segment-empty"><b>${esc(t("graph.no_standard_ai"))}</b><span>${esc(t("graph.all_detected_in_tmux"))}</span></div>`;
|
|
319
|
-
const tmuxSection = tmuxLanes.length
|
|
320
|
-
? `<section class="runtime-segment tmux-runtime" data-runtime-segment="tmux">
|
|
321
|
-
<header>
|
|
322
|
-
<span class="runtime-segment-icon">▦</span>
|
|
323
|
-
<span><small>${esc(t("graph.tmux_only"))}</small><b>${esc(t("graph.tmux_sessions"))}</b><em>${esc(t("graph.tmux_runtime_description"))}</em></span>
|
|
324
|
-
<strong>${esc(t("common.count", { count: tmuxRoots.length }))}</strong>
|
|
325
|
-
<button type="button" class="live-tmux-overview-open">${esc(t("graph.open_tmux_overview"))}</button>
|
|
326
|
-
</header>
|
|
327
|
-
<div class="agent-flow-overview">${tmuxLanes.map((item) => providerFlowLane(item.providerId, item.roots, model)).join("")}</div>
|
|
328
|
-
</section>`
|
|
329
|
-
: "";
|
|
330
|
-
return `<div class="agent-runtime-split" data-runtime-split="true">
|
|
331
|
-
${tmuxSection}
|
|
332
|
-
<section class="runtime-segment standard-runtime" data-runtime-segment="standard">
|
|
333
|
-
<header>
|
|
334
|
-
<span class="runtime-segment-icon">›_</span>
|
|
335
|
-
<span><small>${esc(t("graph.without_tmux"))}</small><b>${esc(t("graph.standard_sessions"))}</b><em>${esc(t("graph.standard_runtime_description"))}</em></span>
|
|
336
|
-
<strong>${esc(t("common.count", { count: standardRoots.length }))}</strong>
|
|
337
|
-
</header>
|
|
338
|
-
${standardHtml}
|
|
339
|
-
</section>
|
|
483
|
+
return `<div class="control-room-overview" data-control-room-overview="true">
|
|
484
|
+
${sortGraphNodes(roots).map(root => controlRoomSession(root, model)).join("")}
|
|
340
485
|
</div>`;
|
|
341
486
|
}
|
|
342
487
|
|
|
@@ -654,6 +799,7 @@ window.LoadToAgentAppFactories.createGraphView = function createGraphView(contex
|
|
|
654
799
|
|
|
655
800
|
return {
|
|
656
801
|
graphNode, compactGraphNode, providerFlowLane, workflowCompactNode, liveTmuxEntries, liveTmuxPaneCard, runtimeSeparatedOverview,
|
|
802
|
+
controlRoomIntent, controlRoomSummary, controlRoomAgentGoal, inferredExecutionSummary,
|
|
657
803
|
workflowMetrics, workflowChildrenSummary, splitSubagents, completedSubagentDisclosure, agentPathTaskName, communicationEndpoint,
|
|
658
804
|
workflowCommunicationPanel, executionActivityLabel, executionActivityStatus, executionActivityCard, executionActivityPanel, focusedGraph,
|
|
659
805
|
};
|