loadtoagent 1.3.3 → 1.3.5

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 (38) hide show
  1. package/main.js +53 -8
  2. package/package.json +5 -2
  3. package/renderer/app-agent-actions.js +172 -66
  4. package/renderer/app-bootstrap.js +3 -1
  5. package/renderer/app-dashboard.js +36 -7
  6. package/renderer/app-drawer-content.js +177 -59
  7. package/renderer/app-drawer-data.js +7 -3
  8. package/renderer/app-drawer.js +72 -33
  9. package/renderer/app-events-dialogs.js +8 -1
  10. package/renderer/app-events-navigation.js +7 -0
  11. package/renderer/app-events-sessions.js +136 -13
  12. package/renderer/app-graph-model.js +2 -5
  13. package/renderer/app-graph-orchestration.js +7 -18
  14. package/renderer/app-graph-view.js +194 -48
  15. package/renderer/app-management.js +330 -35
  16. package/renderer/app-quality.js +5 -0
  17. package/renderer/app-session-render.js +20 -83
  18. package/renderer/app.js +5 -1
  19. package/renderer/i18n-messages.js +232 -21
  20. package/renderer/index.html +9 -6
  21. package/renderer/styles-components.css +140 -0
  22. package/renderer/styles-control-room.css +1439 -0
  23. package/renderer/styles-management.css +390 -3
  24. package/renderer/styles-responsive-shell.css +5 -0
  25. package/renderer/styles-runtime-overview.css +20 -0
  26. package/renderer/terminal-agent.js +31 -7
  27. package/renderer/terminal.js +4 -1
  28. package/src/agentMonitor/claudeParser.js +8 -4
  29. package/src/agentMonitor/codexParser.js +5 -4
  30. package/src/agentMonitor/genericParser.js +7 -6
  31. package/src/agentMonitor.js +44 -4
  32. package/src/attentionNotifier.js +3 -0
  33. package/src/ipc/registerTerminalIpc.js +2 -2
  34. package/src/monitorWorker.js +1 -1
  35. package/src/nodePtyRuntime.js +58 -0
  36. package/src/processMonitor.js +68 -7
  37. package/src/terminalHost.js +104 -5
  38. package/src/terminalManager.js +21 -3
@@ -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, openSessionOrigin, saveDashboardPreferences = () => {},
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) return openDrawer(open.dataset.openSession);
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) return openDrawer(open.dataset.openSession);
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 [...sessions].sort((a, b) => {
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 = `<details class="runtime-disclosure" data-disclosure-key="home:runtime-overview" open>
55
- <summary>
56
- <span>
57
- <b>${esc(t("graph.ai_working_count", { count: runtime.activeCount }))}</b>
58
- <small>${esc(t("graph.runtime_count_explanation", { standard: runtime.standardCount, tmux: runtime.tmuxCount, helpers: runtime.activeHelperCount }))}${runtime.runningExecutionCount ? ` · ${esc(t("graph.runtime_execution_count", { shell: runtime.shellExecutionCount, background: runtime.backgroundExecutionCount }))}` : ""}</small>
59
- </span>
60
- <em>${esc(t("graph.view_detailed_flow"))} <i aria-hidden="true">↓</i>
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
- const tmuxEntries = liveTmuxEntries(state.snapshot && state.snapshot.tmux);
293
- const tmuxLinkedIds = new Set(tmuxEntries.map((entry) => String(entry.agent && entry.agent.linkedSessionId || "")).filter(Boolean));
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
  };