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,544 @@
1
+ "use strict";
2
+
3
+ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
+
5
+ window.LoadToAgentAppFactories.createGraphView = function createGraphView(context = {}) {
6
+ const {
7
+ $,
8
+ esc,
9
+ state,
10
+ STATUS,
11
+ readablePreview,
12
+ compact,
13
+ timeAgo,
14
+ timeOnly,
15
+ providerInfo,
16
+ providerStyle,
17
+ agentRoleLabel,
18
+ statusClass,
19
+ currentActivity,
20
+ isLiveSession,
21
+ subagentWorkState,
22
+ subagentWorkLabel,
23
+ latestWorkCopy,
24
+ statusIcon,
25
+ sortGraphNodes,
26
+ graphChildren,
27
+ agentExecutionMode,
28
+ executionModeBadge,
29
+ graphDescendantCount,
30
+ } = context;
31
+
32
+ function graphNode(session, options = {}) {
33
+ const provider = providerInfo(session.provider);
34
+ const activity = currentActivity(session);
35
+ const context = session.context || {};
36
+ const usage = session.usage || {};
37
+ const percent = Math.max(0, Math.min(100, Number(context.percent || 0)));
38
+ const running = isLiveSession(session);
39
+ const childCount = (session.childIds || []).length;
40
+ const childMetrics = session.collaboration && session.collaboration.metrics;
41
+ const cumulativeChildren = childMetrics ? childMetrics.cumulativeCreated : childCount;
42
+ const delegation = session.delegation || {};
43
+ const displayedTask =
44
+ session.parentId && delegation.assignmentObserved && delegation.assignment
45
+ ? delegation.assignment
46
+ : session.parentId && (delegation.taskName || session.taskName)
47
+ ? delegation.taskName || session.taskName
48
+ : session.title;
49
+ const goalPreview = readablePreview(displayedTask, options.focus ? 118 : 96);
50
+ const currentWork = latestWorkCopy(session);
51
+ const currentPreview = readablePreview(currentWork, options.focus ? 132 : 108);
52
+ const role = session.parentId
53
+ ? `도움 AI${session.agentName ? ` · ${session.agentName}` : ""}${session.agentRole ? ` / ${agentRoleLabel(session.agentRole)}` : ""}`
54
+ : "일을 맡은 AI";
55
+ return `<article class="agent-node ${running ? "running" : ""} ${session.parentId ? "child-agent" : "root-agent"} ${options.focus ? "is-focus" : ""}"
56
+ data-motion-key="agent:${esc(session.id)}"
57
+ data-motion-value="${esc(session.updatedAt || "")}:${usage.total || 0}:${esc(session.status || "")}"
58
+ style="${providerStyle(session.provider)}">
59
+ <button class="agent-node-main" type="button" data-graph-focus="${esc(session.id)}" aria-label="${esc(role)} 관계 중심으로 보기">
60
+ <span class="agent-node-top">
61
+ <span class="provider-mark">${esc(provider.mark)}</span>
62
+ <span class="agent-identity"><b>${esc(role)}</b><small>${esc(provider.label)} · ${esc(session.model || "모델 정보 없음")}</small></span>
63
+ ${executionModeBadge(session, true)}
64
+ <span class="status-pill ${statusClass(session.status)}">${esc(STATUS[session.status] || session.status)}</span>
65
+ </span>
66
+ <span class="agent-task-label">
67
+ ${session.parentId ? `담당 작업${delegation.assignmentSource === "parent-narration" ? " · 메인 AI 설명 기반" : ""}` : "지금 목표"}
68
+ </span>
69
+ <strong class="agent-task" title="${esc(goalPreview.full)}">${esc(goalPreview.text)}</strong>
70
+ ${goalPreview.truncated ? '<span class="agent-goal-note">요약 표시 · 전체 내용은 대화 상세에서 확인</span>' : ""}
71
+ <span class="agent-current">
72
+ <span><i>${statusIcon(activity.type)}</i><b>지금 하는 일</b></span>
73
+ <strong title="${esc(currentPreview.full)}">${esc(currentPreview.text)}</strong>
74
+ </span>
75
+ <span class="agent-node-metrics">
76
+ <span>
77
+ <small>기억 공간 사용</small>
78
+ <b>${context.window ? `${percent.toFixed(1)}%` : "--"}</b>
79
+ </span>
80
+ <span>
81
+ <small>${window.LoadToAgentI18n.t("ui.tokens_used_2")}</small>
82
+ <b>${compact(usage.total)}</b>
83
+ </span>
84
+ <span>
85
+ <small>마지막 활동</small>
86
+ <b>${esc(timeAgo(session.updatedAt))}</b>
87
+ </span>
88
+ </span>
89
+ <span class="agent-node-gauge"><i style="width:${percent}%"></i></span>
90
+ </button>
91
+ <footer class="agent-node-footer">
92
+ <span>${cumulativeChildren ? `서브에이전트 ${cumulativeChildren}개 누적 생성` : session.parentId ? "도움을 맡은 AI" : "이 작업의 중심 AI"}</span>
93
+ <button type="button" data-open-session="${esc(session.id)}">대화 내용 보기 <b>↗</b>
94
+ </button>
95
+ </footer>
96
+ </article>`;
97
+ }
98
+
99
+ function compactGraphNode(session, model, label = "") {
100
+ const provider = providerInfo(session.provider);
101
+ const usage = session.usage || {};
102
+ const directChildren = graphChildren(session, model).length;
103
+ const identity = session.parentId ? `도움 AI ${session.agentName || agentRoleLabel(session.agentRole)}` : session.workspace || "중심 작업";
104
+ const delegation = session.delegation || {};
105
+ const taskName = delegation.taskName || session.taskName || "";
106
+ const assignedWork = delegation.assignmentObserved && delegation.assignment ? delegation.assignment : taskName || session.title;
107
+ const sharedGoal = delegation.sharedGoal || session.sharedGoal || "";
108
+ const outcome = delegation.result || session.result || "";
109
+ const outcomeText = outcome || latestWorkCopy(session);
110
+ const assignedWorkPreview = readablePreview(assignedWork, session.parentId ? 110 : 104);
111
+ const taskLabel = session.parentId ? `${label || agentRoleLabel(session.agentRole)}${taskName ? ` · 담당 ${taskName}` : ""}` : label;
112
+ const assignmentSourceNote =
113
+ session.parentId && delegation.assignmentSource === "parent-narration"
114
+ ? '<span class="agent-flow-assignment-source">메인 AI가 작업 시작 직전에 설명한 내용</span>'
115
+ : "";
116
+ const sharedGoalCopy =
117
+ session.parentId && sharedGoal && sharedGoal !== assignedWork ? `<span class="agent-flow-shared">공유 목표 · ${esc(sharedGoal)}</span>` : "";
118
+ const outcomeCopy = session.parentId
119
+ ? `<span class="agent-flow-outcome ${session.status === "completed" ? "done" : ""}">
120
+ <b>${session.status === "completed" ? "완료 결과" : "현재 작업"}</b>
121
+ <span class="agent-flow-outcome-copy" title="${esc(outcomeText)}">${esc(outcomeText)}</span>
122
+ </span>`
123
+ : "";
124
+ if (session.parentId) {
125
+ const primaryTask = taskName || assignedWork || session.title;
126
+ const assignmentCopy =
127
+ assignedWork && assignedWork !== primaryTask
128
+ ? `<span class="agent-flow-assignment"><small>담당 내용</small><strong title="${esc(assignedWork)}">${esc(assignedWork)}</strong></span>`
129
+ : "";
130
+ const workState = subagentWorkState(session);
131
+ const interaction = directChildren
132
+ ? `data-graph-focus="${esc(session.id)}" aria-label="${esc(primaryTask)}의 하위 서브에이전트 흐름 보기"`
133
+ : `data-open-subagent-chat="${esc(session.id)}" aria-label="${esc(primaryTask)}와 메인 AI의 대화 보기"`;
134
+ const action = directChildren ? `하위 서브에이전트 ${directChildren}개 보기 →` : "메인 AI와의 대화 보기 →";
135
+ return `<button type="button" class="agent-flow-row child-session work-${workState} ${statusClass(session.status)}"
136
+ ${interaction}
137
+ data-motion-key="agent:${esc(session.id)}"
138
+ data-motion-value="${esc(session.updatedAt || "")}:${usage.total || 0}:${esc(session.status || "")}"
139
+ style="${providerStyle(session.provider)}">
140
+ <span class="agent-flow-state" aria-hidden="true"></span>
141
+ <span class="agent-flow-copy">
142
+ <span class="agent-flow-kicker">
143
+ <small>${esc(label || agentRoleLabel(session.agentRole))} 세션</small>
144
+ <time>${esc(timeAgo(session.updatedAt))}</time>
145
+ </span>
146
+ <b class="agent-flow-session-title" title="${esc(primaryTask)}">${esc(primaryTask)}</b>
147
+ <span class="agent-flow-agent">
148
+ <i>${esc(provider.mark)}</i>
149
+ <strong>${esc(session.agentName || "이름 미확인")}</strong>
150
+ <small>${esc(provider.label)}${session.model ? ` · ${esc(session.model)}` : ""}</small>
151
+ </span>
152
+ ${assignmentCopy}${assignmentSourceNote}${outcomeCopy}<span class="agent-flow-child-action">${esc(action)}</span>
153
+ </span>
154
+ <span class="agent-flow-provider">
155
+ ${executionModeBadge(session, true)}
156
+ <small class="status-pill work-${workState}">${esc(subagentWorkLabel(session))}</small>
157
+ ${session.status === "completed" ? "<em>최근 작업 완료</em>" : ""}
158
+ </span>
159
+ </button>`;
160
+ }
161
+ return `<button type="button" class="agent-flow-row ${isLiveSession(session) ? "running" : ""} ${statusClass(session.status)}"
162
+ data-graph-focus="${esc(session.id)}"
163
+ data-motion-key="agent:${esc(session.id)}"
164
+ data-motion-value="${esc(session.updatedAt || "")}:${usage.total || 0}:${esc(session.status || "")}"
165
+ style="${providerStyle(session.provider)}">
166
+ <span class="agent-flow-state" aria-hidden="true"></span>
167
+ <span class="agent-flow-copy">
168
+ ${taskLabel ? `<small>${esc(taskLabel)}</small>` : ""}
169
+ <b title="${esc(assignedWorkPreview.full)}">${esc(assignedWorkPreview.text)}</b>
170
+ <em>${esc(identity)} · ${directChildren ? `도움 AI ${directChildren}명 · ` : ""}${esc(timeAgo(session.updatedAt))}</em>
171
+ ${assignmentSourceNote}${sharedGoalCopy}${outcomeCopy}
172
+ </span>
173
+ <span class="agent-flow-provider"><i>${esc(provider.mark)}</i><small>${esc(STATUS[session.status] || session.status)}</small></span>
174
+ </button>`;
175
+ }
176
+
177
+ function providerFlowLane(providerId, roots, model) {
178
+ const provider = providerInfo(providerId);
179
+ const ordered = sortGraphNodes(roots);
180
+ const expanded = state.graphExpandedProviders.has(providerId);
181
+ const shown = expanded ? ordered : ordered.slice(0, 6);
182
+ const hidden = Math.max(0, ordered.length - shown.length);
183
+ const agents = ordered.reduce((total, root) => total + 1 + graphDescendantCount(root, model), 0);
184
+ return `<section class="agent-flow-lane" style="${providerStyle(providerId)}">
185
+ <header class="agent-flow-lane-head">
186
+ <span class="provider-mark">${esc(provider.mark)}</span>
187
+ <span><b>${esc(provider.label)}</b><small>${ordered.length}개 큰 일 · 참여 AI ${agents}명</small></span>
188
+ <em>${ordered.filter(isLiveSession).length}개 진행 중</em>
189
+ </header>
190
+ <div class="agent-flow-list">${shown.map((root) => compactGraphNode(root, model)).join("")}</div>
191
+ ${
192
+ hidden
193
+ ? `<button type="button" class="agent-flow-more" data-graph-provider-more="${esc(providerId)}">나머지 ${hidden}개 일도 보기</button>`
194
+ : expanded && ordered.length > 6
195
+ ? `<button type="button" class="agent-flow-more" data-graph-provider-less="${esc(providerId)}">간단히 보기</button>`
196
+ : ""
197
+ }
198
+ </section>`;
199
+ }
200
+
201
+ function workflowCompactNode(session, model, side, label) {
202
+ const port = side === "upstream" ? '<span class="agent-workflow-port output" data-workflow-port="upstream-output" aria-hidden="true"></span>' : "";
203
+ return `<div class="agent-workflow-node ${side}" data-workflow-node="${esc(session.id)}">${port}${compactGraphNode(session, model, label)}</div>`;
204
+ }
205
+
206
+ function liveTmuxEntries(tmux = state.snapshot && state.snapshot.tmux) {
207
+ const entries = [];
208
+ for (const distro of (tmux && tmux.distros) || []) {
209
+ for (const tmuxSession of distro.sessions || []) {
210
+ for (const window of tmuxSession.windows || []) {
211
+ for (const pane of window.panes || []) {
212
+ if (!pane.agent) continue;
213
+ entries.push({ distro, tmuxSession, window, pane, agent: pane.agent });
214
+ }
215
+ }
216
+ }
217
+ }
218
+ return entries.sort(
219
+ (a, b) =>
220
+ Number(b.pane.active) - Number(a.pane.active) ||
221
+ Number(a.pane.dead) - Number(b.pane.dead) ||
222
+ String(a.tmuxSession.name).localeCompare(String(b.tmuxSession.name)),
223
+ );
224
+ }
225
+
226
+ function liveTmuxPaneCard(entry) {
227
+ const { distro, tmuxSession, window, pane, agent } = entry;
228
+ const provider = providerInfo(agent.provider);
229
+ const linked = agent.linkedSessionId ? (state.snapshot?.sessions || []).find((session) => session.id === agent.linkedSessionId) || null : null;
230
+ const title = linked ? linked.title : pane.title || `${provider.label} TMUX 작업`;
231
+ const stateLabel = pane.dead ? "종료됨" : pane.active ? "현재 선택된 칸" : "백그라운드 실행";
232
+ return `<article class="live-tmux-card ${pane.active ? "active" : ""} ${pane.dead ? "dead" : ""}"
233
+ style="${providerStyle(agent.provider)}"
234
+ data-motion-key="live-tmux:${esc(pane.id)}"
235
+ data-motion-value="${esc(agent.updatedAt || "")}:${pane.pid || 0}">
236
+ <button type="button" class="live-tmux-pane" data-tmux-type="pane" data-tmux-id="${esc(pane.id)}" aria-label="${esc(tmuxSession.name)} TMUX 칸 열기">
237
+ <span class="live-tmux-card-head">
238
+ <span class="live-tmux-symbol">▦</span><span><small>TMUX 세션</small><b>${esc(tmuxSession.name)}</b></span>
239
+ <em>${esc(stateLabel)}</em>
240
+ </span>
241
+ <strong class="live-tmux-title">${esc(title)}</strong>
242
+ <span class="live-tmux-agent">
243
+ <i>${esc(provider.mark)}</i><b>${esc(provider.label)}</b>
244
+ <small>${esc(agent.command || pane.command || "AI")}</small>
245
+ </span>
246
+ <span class="live-tmux-location">
247
+ <b>${esc(distro.name)}</b>
248
+ <i>›</i>
249
+ <span>${esc(window.name || `창 ${window.index + 1}`)}</span>
250
+ <i>›</i>
251
+ <span>칸 ${pane.index + 1} · ${esc(pane.nativeId || pane.id)}</span>
252
+ </span>
253
+ <span class="live-tmux-cwd" title="${esc(pane.cwd || "")}">${esc(pane.cwd || "작업 폴더 미확인")}</span>
254
+ </button>
255
+ <footer>
256
+ <span>${linked ? "대화 기록과 연결됨" : "TMUX 프로세스에서 직접 감지"}</span>
257
+ <span>
258
+ ${linked ? `<button type="button" data-graph-focus="${esc(linked.id)}">AI 흐름 보기</button>` : ""}
259
+ <button type="button" class="live-tmux-pane" data-tmux-type="pane" data-tmux-id="${esc(pane.id)}">TMUX에서 열기 →</button>
260
+ </span>
261
+ </footer>
262
+ </article>`;
263
+ }
264
+
265
+ function runtimeSeparatedOverview(roots, model) {
266
+ const tmux = (state.snapshot && state.snapshot.tmux) || { distros: [], summary: {} };
267
+ const tmuxEntries = liveTmuxEntries(tmux);
268
+ const tmuxLinkedIds = new Set(tmuxEntries.map((entry) => entry.agent.linkedSessionId).filter(Boolean));
269
+ const tmuxRoots = roots.filter((root) => agentExecutionMode(root).kind === "tmux");
270
+ const standardRoots = roots.filter((root) => agentExecutionMode(root).kind !== "tmux");
271
+ const providerOrder = [...new Set([...state.providers.map((item) => item.id), ...roots.map((item) => item.provider)])];
272
+ const lanesFor = (items) =>
273
+ providerOrder.map((providerId) => ({ providerId, roots: items.filter((root) => root.provider === providerId) })).filter((item) => item.roots.length);
274
+ const standardLanes = lanesFor(standardRoots);
275
+ const fallbackTmuxLanes = lanesFor(tmuxRoots.filter((root) => !tmuxLinkedIds.has(root.id)));
276
+ const summary = tmux.summary || {};
277
+ const standardHtml = standardLanes.length
278
+ ? `<div class="agent-flow-overview">${standardLanes.map((item) => providerFlowLane(item.providerId, item.roots, model)).join("")}</div>`
279
+ : '<div class="runtime-segment-empty"><b>일반 실행 AI가 없습니다</b><span>현재 감지된 작업은 모두 TMUX에서 실행 중입니다.</span></div>';
280
+ const tmuxHtml =
281
+ tmuxEntries.length || fallbackTmuxLanes.length
282
+ ? `${tmuxEntries.length ? `<div class="live-tmux-grid">${tmuxEntries.map(liveTmuxPaneCard).join("")}</div>` : ""}
283
+ ${
284
+ fallbackTmuxLanes.length
285
+ ? `<div class="agent-flow-overview live-tmux-fallback">
286
+ ${fallbackTmuxLanes.map((item) => providerFlowLane(item.providerId, item.roots, model)).join("")}
287
+ </div>`
288
+ : ""
289
+ }`
290
+ : '<div class="runtime-segment-empty tmux"><b>TMUX에서 실행 중인 AI가 없습니다</b><span>TMUX AI 프로세스가 감지되면 일반 실행과 분리해 여기에 표시합니다.</span></div>';
291
+ return `<div class="agent-runtime-split" data-runtime-split="true">
292
+ <section class="runtime-segment tmux-runtime" data-runtime-segment="tmux">
293
+ <header>
294
+ <span class="runtime-segment-icon">▦</span>
295
+ <span><small>TMUX 전용</small><b>TMUX 세션</b><em>Linux 작업 묶음·창·분할 칸을 유지해서 실행 중인 AI</em></span>
296
+ <strong>${tmuxEntries.length || summary.aiPanes || 0}개</strong>
297
+ <button type="button" class="live-tmux-overview-open">TMUX 전체 화면 →</button>
298
+ </header>
299
+ ${tmuxHtml}
300
+ </section>
301
+ <section class="runtime-segment standard-runtime" data-runtime-segment="standard">
302
+ <header>
303
+ <span class="runtime-segment-icon">›_</span>
304
+ <span><small>TMUX 미사용</small><b>일반 실행 세션</b><em>Codex 앱·외부 터미널에서 실행 중인 메인 AI</em></span>
305
+ <strong>${standardRoots.length}개</strong>
306
+ </header>
307
+ ${standardHtml}
308
+ </section>
309
+ </div>`;
310
+ }
311
+
312
+ function workflowMetrics(session, children) {
313
+ const observed = session.collaboration && session.collaboration.metrics;
314
+ return (
315
+ observed || {
316
+ cumulativeCreated: children.length,
317
+ simultaneousCapacity: 0,
318
+ currentlyRunning: children.filter(isLiveSession).length,
319
+ completedRecords: children.filter((child) => child.status === "completed").length,
320
+ retainedCount: null,
321
+ capacitySource: "unknown",
322
+ cumulativeSource: "child-sessions",
323
+ }
324
+ );
325
+ }
326
+
327
+ function workflowChildrenSummary(session, children) {
328
+ if (!children.length && !(session.collaboration && session.collaboration.metrics)) return "";
329
+ const counts = new Map();
330
+ for (const child of children) counts.set(child.provider, (counts.get(child.provider) || 0) + 1);
331
+ const providers = [...counts.entries()]
332
+ .map(([providerId, count]) => {
333
+ const provider = providerInfo(providerId);
334
+ return `<span class="workflow-summary-chip" style="${providerStyle(providerId)}">
335
+ <i>${esc(provider.mark)}</i>
336
+ <b>${esc(provider.label)}</b>
337
+ <em>${count}</em>
338
+ </span>`;
339
+ })
340
+ .join("");
341
+ const metrics = workflowMetrics(session, children);
342
+ const capacity = metrics.simultaneousCapacity > 0 ? metrics.simultaneousCapacity : "--";
343
+ const retained = metrics.retainedCount == null ? "현재 목록 유지 수는 관측되지 않음" : `현재 런타임 목록에는 ${metrics.retainedCount}개 유지`;
344
+ const source = metrics.capacitySource === "runtime-instruction" ? "세션 런타임 한도" : "동시 한도 출처 미확인";
345
+ return `<div class="agent-workflow-summary" data-collaboration-summary="true">
346
+ <div class="workflow-metric-grid">
347
+ <span data-collaboration-metric="created">
348
+ <small>이 작업에서 누적 생성</small><b>${esc(metrics.cumulativeCreated)}</b><em>${window.LoadToAgentI18n.t("ui.items")}</em>
349
+ </span>
350
+ <span data-collaboration-metric="capacity">
351
+ <small>동시에 유지 가능</small><b>${esc(capacity)}</b><em>${capacity === "--" ? "" : window.LoadToAgentI18n.t("ui.items")}</em>
352
+ </span>
353
+ <span data-collaboration-metric="running">
354
+ <small>현재 실행 중</small><b>${esc(metrics.currentlyRunning)}</b><em>${window.LoadToAgentI18n.t("ui.items")}</em>
355
+ </span>
356
+ <span data-collaboration-metric="completed">
357
+ <small>작업 완료 기록</small><b>${esc(metrics.completedRecords)}</b><em>${window.LoadToAgentI18n.t("ui.items")}</em>
358
+ </span>
359
+ </div>
360
+ <div class="workflow-summary-evidence"><span>${esc(retained)} · 완료 기록은 삭제하지 않고 기본으로 접어 보관</span><small>${esc(source)} · spawn/완료 이벤트 기준</small></div>
361
+ <div class="workflow-summary-providers">${providers}</div>
362
+ </div>`;
363
+ }
364
+
365
+ function splitSubagents(children) {
366
+ return children.reduce(
367
+ (out, session) => {
368
+ if (session.status === "completed" || session.completionObserved) out.completed.push(session);
369
+ else out.ongoing.push(session);
370
+ return out;
371
+ },
372
+ { ongoing: [], completed: [] },
373
+ );
374
+ }
375
+
376
+ function completedSubagentDisclosure(ownerId, completed, expanded) {
377
+ if (!completed.length) return "";
378
+ return `<div class="completed-subagent-disclosure ${expanded ? "expanded" : ""}" data-completed-subagent-section>
379
+ <button type="button" data-subagent-completed-toggle="${esc(ownerId)}" aria-expanded="${expanded ? "true" : "false"}">
380
+ <span class="completed-disclosure-icon">✓</span>
381
+ <span><b>완료된 서브에이전트 ${completed.length}개</b>
382
+ <small>${expanded ? "완료 기록을 펼쳐 보는 중" : "작업 중인 AI에 집중할 수 있도록 기본으로 접어둡니다"}</small>
383
+ </span>
384
+ <i>${expanded ? "접기 ↑" : "펼쳐 보기 ↓"}</i>
385
+ </button>
386
+ </div>`;
387
+ }
388
+
389
+ function agentPathTaskName(value) {
390
+ return (
391
+ String(value || "")
392
+ .replace(/\\/g, "/")
393
+ .replace(/\/$/, "")
394
+ .split("/")
395
+ .filter(Boolean)
396
+ .pop() || ""
397
+ );
398
+ }
399
+
400
+ function communicationEndpoint(value, owner, model) {
401
+ const path = String(value || "");
402
+ if (!path) return "대상 미상";
403
+ if (path === "Codex 런타임") return path;
404
+ if (path === "/root" || path === owner.agentPath || (!owner.agentPath && path === owner.id)) return "메인 AI";
405
+ const taskName = agentPathTaskName(path);
406
+ const session = model.nodes.find((node) => node.agentPath === path || node.taskName === taskName);
407
+ if (session) return `${session.agentName || "서브 AI"}${taskName ? ` · ${taskName}` : ""}`;
408
+ return taskName || path;
409
+ }
410
+
411
+ function workflowCommunicationPanel(focus, parent, model) {
412
+ const owner = focus;
413
+ const all = (owner.collaboration && owner.collaboration.communications) || [];
414
+ const relevant = all.filter((event) => ["assignment", "started", "followup", "message", "result", "interrupt"].includes(event.kind));
415
+ const events = relevant.slice(-60);
416
+ if (!events.length) {
417
+ return `<section class="agent-communication-panel empty" data-collaboration-communications="0">
418
+ <header>
419
+ <span>
420
+ <b>메인 AI ↔ 서브에이전트 소통</b>
421
+ <small>배정·추가 지시·결과 반환 기록</small>
422
+ </span>
423
+ </header>
424
+ <p>이 세션 로그에서는 에이전트 간 통신 이벤트가 확인되지 않았습니다.</p>
425
+ </section>`;
426
+ }
427
+ const rows = events
428
+ .map((event) => {
429
+ const text =
430
+ event.text ||
431
+ (event.protected
432
+ ? `${event.taskName || "이 작업"}을 서브에이전트에게 배정했습니다.`
433
+ : event.kind === "started"
434
+ ? "런타임에서 실행 시작을 확인했습니다."
435
+ : "내용 없이 상태만 기록되었습니다.");
436
+ const sourceLabel = event.assignmentSource === "parent-narration" ? " · 작업 시작 직전 메인 AI 설명" : "";
437
+ return `<article class="agent-communication-event ${esc(event.kind)}" data-communication-kind="${esc(event.kind)}">
438
+ <span class="communication-route">
439
+ <b>${esc(communicationEndpoint(event.from, owner, model))}</b><i>→</i>
440
+ <b>${esc(communicationEndpoint(event.to, owner, model))}</b>
441
+ </span>
442
+ <span class="communication-copy">
443
+ <small>${esc(event.label)}${event.taskName ? ` · ${esc(event.taskName)}` : ""}${sourceLabel}</small>
444
+ <strong>${esc(text)}</strong>
445
+ </span>
446
+ <time>${esc(timeOnly(event.timestamp))}</time>
447
+ </article>`;
448
+ })
449
+ .join("");
450
+ const countLabel = relevant.length > events.length ? `최근 ${events.length} / 전체 ${relevant.length}건` : `${events.length}건`;
451
+ return `<section class="agent-communication-panel"
452
+ data-collaboration-communications="${events.length}"
453
+ data-collaboration-communications-total="${relevant.length}">
454
+ <header>
455
+ <span>
456
+ <b>메인 AI ↔ 서브에이전트 소통</b>
457
+ <small>누가 일을 맡겼고, 언제 시작했으며, 어떤 결과를 돌려줬는지 시간순으로 표시</small>
458
+ </span>
459
+ <em>${countLabel}</em>
460
+ </header>
461
+ <div class="agent-communication-list">${rows}</div>
462
+ </section>`;
463
+ }
464
+
465
+ function focusedGraph(focus, model, motionKind = "refresh") {
466
+ const parent = focus.parentId ? model.byId.get(focus.parentId) : null;
467
+ const children = graphChildren(focus, model);
468
+ const { ongoing, completed } = splitSubagents(children);
469
+ const completedExpanded = state.expandedCompletedSubagents.has(focus.id);
470
+ const shownChildren = completedExpanded ? [...ongoing, ...completed] : ongoing;
471
+ const metrics = workflowMetrics(focus, children);
472
+ const upstream = parent
473
+ ? workflowCompactNode(parent, model, "upstream", parent.parentId ? "이전 AI로 돌아가기" : "메인 AI로 돌아가기")
474
+ : `<div class="agent-workflow-origin">
475
+ <span class="workflow-origin-icon">◎</span>
476
+ <span>
477
+ <b>사용자 요청</b>
478
+ <small>이 작업이 시작된 곳</small>
479
+ </span>
480
+ <span class="agent-workflow-port output" data-workflow-port="upstream-output" aria-hidden="true">
481
+ </span>
482
+ </div>`;
483
+ const ongoingRows = ongoing.length
484
+ ? ongoing.map((child) => workflowCompactNode(child, model, "downstream", agentRoleLabel(child.agentRole))).join("")
485
+ : children.length
486
+ ? '<div class="agent-workflow-empty current-clear"><b>현재 작업 중인 서브에이전트가 없습니다</b><span>완료된 기록은 아래에서 필요할 때만 펼쳐볼 수 있습니다.</span></div>'
487
+ : '<div class="agent-workflow-empty">아직 다른 AI에게 나눠 맡긴 일이 없습니다.</div>';
488
+ const completedRows = completedExpanded
489
+ ? `<div class="completed-subagent-list" data-completed-subagent-list>
490
+ ${completed.map((child) => workflowCompactNode(child, model, "downstream", agentRoleLabel(child.agentRole))).join("")}
491
+ </div>`
492
+ : "";
493
+ const downstream = `${ongoingRows}${completedSubagentDisclosure(focus.id, completed, completedExpanded)}${completedRows}`;
494
+ const connectMotion = ["focus", "focus-back", "view"].includes(motionKind) ? "motion-connect" : "";
495
+ const childGroupPort = shownChildren.length
496
+ ? '<span class="agent-workflow-port input group-input" data-workflow-port="children-group-input" aria-hidden="true"></span>'
497
+ : "";
498
+ return `<div class="agent-workflow-canvas ${connectMotion}" data-workflow-focus="${esc(focus.id)}">
499
+ <svg class="agent-workflow-edges" role="img" aria-label="일을 맡긴 AI에서 선택한 AI를 거쳐 도움 AI로 이어지는 연결">
500
+ <title>AI 작업 연결</title>
501
+ <desc>왼쪽은 일을 맡긴 곳, 가운데는 선택한 AI, 오른쪽은 나눠 맡긴 AI입니다.</desc>
502
+ <defs><marker id="workflowArrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto" markerUnits="strokeWidth">
503
+ <path d="M0,0 L8,4 L0,8 Z" fill="context-stroke"></path>
504
+ </marker></defs>
505
+ <g data-workflow-paths></g>
506
+ </svg>
507
+ <div class="agent-workflow-grid">
508
+ <section class="agent-workflow-column upstream-column">
509
+ <header><b>${parent ? "이 일을 맡긴 AI" : "작업 시작점"}</b>
510
+ <span>${parent ? "왼쪽을 눌러 이전으로 돌아가요" : "사용자가 처음 맡긴 일"}</span>
511
+ </header>
512
+ <div class="agent-workflow-stack">${upstream}</div>
513
+ </section>
514
+ <section class="agent-workflow-column selected-column">
515
+ <header><b>지금 선택한 AI</b><span>${focus.parentId ? "도움을 나눠 맡은 AI" : "전체 일을 맡은 메인 AI"}</span></header>
516
+ <div class="agent-workflow-selected-stack">
517
+ <div class="agent-workflow-selected">
518
+ <span class="agent-workflow-port input" data-workflow-port="focus-input" aria-hidden="true"></span>
519
+ ${graphNode(focus, { focus: true })}
520
+ </div>
521
+ ${context.agentCommandComposer(focus)}
522
+ ${shownChildren.length ? '<span class="agent-workflow-port output" data-workflow-port="focus-output" aria-hidden="true"></span>' : ""}
523
+ </div>
524
+ </section>
525
+ <section class="agent-workflow-column downstream-column"
526
+ data-workflow-child-count="${children.length}" data-workflow-visible-child-count="${shownChildren.length}">
527
+ ${childGroupPort}
528
+ <header><b>서브에이전트 세션</b>
529
+ <span>진행·대기 ${ongoing.length}개 바로 표시 · 완료 ${completed.length}개 기본 숨김</span>
530
+ </header>
531
+ ${workflowChildrenSummary(focus, children)}
532
+ <div class="agent-workflow-stack downstream-stack ${shownChildren.length > 3 ? "density-many" : ""}">${downstream}</div>
533
+ </section>
534
+ </div>
535
+ ${workflowCommunicationPanel(focus, parent, model)}
536
+ </div>`;
537
+ }
538
+
539
+ return {
540
+ graphNode, compactGraphNode, providerFlowLane, workflowCompactNode, liveTmuxEntries, liveTmuxPaneCard, runtimeSeparatedOverview,
541
+ workflowMetrics, workflowChildrenSummary, splitSubagents, completedSubagentDisclosure, agentPathTaskName, communicationEndpoint,
542
+ workflowCommunicationPanel, focusedGraph,
543
+ };
544
+ };