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,361 @@
1
+ "use strict";
2
+
3
+ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
+
5
+ window.LoadToAgentAppFactories.createDashboard = function createDashboard(context = {}) {
6
+ const {
7
+ $,
8
+ esc,
9
+ uiLocale,
10
+ PROJECTLESS_WORKSPACE,
11
+ state,
12
+ compact,
13
+ providerStyle,
14
+ } = context;
15
+
16
+ function renderProviderRail() {
17
+ $("#providerRail").innerHTML = state.providers
18
+ .map((provider) => {
19
+ const available = !!state.availability[provider.id];
20
+ return `<div class="provider-rail-item ${available ? "connected" : ""}" style="${providerStyle(provider.id)}">
21
+ <span class="provider-mini-mark">${esc(provider.mark)}</span><strong>${esc(provider.label)}</strong>
22
+ <small>${available ? window.LoadToAgentI18n.t("ui.cli_found") : window.LoadToAgentI18n.t("ui.setup_required")}</small>
23
+ <span class="connection-dot"></span>
24
+ </div>`;
25
+ })
26
+ .join("");
27
+ }
28
+
29
+ function isProjectlessSession(session) {
30
+ if (!session || !session.cwd) return true;
31
+ if (typeof session.projectless === "boolean") return session.projectless;
32
+ const normalized = String(session.cwd).replace(/\\/g, "/").replace(/\/+$/, "");
33
+ return session.provider === "codex" && session.clientKind === "codex-desktop" && /(?:^|\/)Documents\/Codex\/\d{4}-\d{2}-\d{2}\/new-chat$/i.test(normalized);
34
+ }
35
+
36
+ function sessionWorkspaceLabel(session) {
37
+ return isProjectlessSession(session) ? window.LoadToAgentI18n.t("ui.no_project") : (session && session.workspace) || "작업 폴더 미상";
38
+ }
39
+
40
+ function matchesWorkspaceFilter(session) {
41
+ if (state.workspace === "all") return true;
42
+ if (state.workspace === PROJECTLESS_WORKSPACE) return isProjectlessSession(session);
43
+ return (
44
+ !isProjectlessSession(session) &&
45
+ String((session && session.cwd) || "")
46
+ .toLowerCase()
47
+ .startsWith(state.workspace.toLowerCase())
48
+ );
49
+ }
50
+
51
+ function renderWorkspaces() {
52
+ const list = $("#workspaceList");
53
+ const projectlessCount = ((state.snapshot && state.snapshot.sessions) || []).filter((session) => !session.parentId && isProjectlessSession(session)).length;
54
+ list.innerHTML =
55
+ `<button type="button" class="workspace-item ${state.workspace === "all" ? "selected" : ""}"
56
+ data-workspace="all" aria-pressed="${state.workspace === "all" ? "true" : "false"}">
57
+ <strong>${window.LoadToAgentI18n.t("ui.all_workspaces")}</strong>
58
+ </button>` +
59
+ (projectlessCount
60
+ ? `<button type="button" class="workspace-item projectless ${state.workspace === PROJECTLESS_WORKSPACE ? "selected" : ""}"
61
+ data-workspace="${PROJECTLESS_WORKSPACE}"
62
+ title="${esc(window.LoadToAgentI18n.t("ui.session_not_linked_to_a_specific_project"))}"
63
+ aria-pressed="${state.workspace === PROJECTLESS_WORKSPACE ? "true" : "false"}">
64
+ <strong>${window.LoadToAgentI18n.t("ui.no_project")}</strong>
65
+ <small>${projectlessCount}</small>
66
+ </button>`
67
+ : "") +
68
+ state.workspaces
69
+ .map(
70
+ (item) => `<div class="workspace-row">
71
+ <button type="button" class="workspace-item ${state.workspace === item.path ? "selected" : ""}"
72
+ data-workspace="${esc(item.path)}" title="${esc(item.path)}"
73
+ aria-pressed="${state.workspace === item.path ? "true" : "false"}">
74
+ <strong>${esc(item.name)}</strong>
75
+ </button>
76
+ <button type="button" class="workspace-remove" data-remove-workspace="${esc(item.path)}"
77
+ aria-label="${esc(item.name)} 작업 폴더를 목록에서 제거"
78
+ title="${esc(window.LoadToAgentI18n.t("ui.remove_from_list"))}">×</button>
79
+ </div>`,
80
+ )
81
+ .join("") +
82
+ (!state.workspaces.length ? `<div class="workspace-empty">${window.LoadToAgentI18n.t("ui.use_the_button_to_add_frequently_used_workspaces")}</div>` : "");
83
+ }
84
+
85
+ function renderGlobalStats() {
86
+ const totals = (state.snapshot && state.snapshot.summary && state.snapshot.summary.totals) || {};
87
+ const sessions = (state.snapshot && state.snapshot.sessions) || [];
88
+ const rootCount = sessions.filter((session) => !session.parentId).length;
89
+ const helperCount = sessions.filter((session) => session.parentId).length;
90
+ const items = [
91
+ [window.LoadToAgentI18n.t("ui.all_tasks"), rootCount, window.LoadToAgentI18n.t("ui.items"), ""],
92
+ [window.LoadToAgentI18n.t("ui.ai_working_now"), totals.active || 0, window.LoadToAgentI18n.t("ui.items"), "live"],
93
+ [window.LoadToAgentI18n.t("ui.waiting_for_review"), totals.waiting || 0, window.LoadToAgentI18n.t("ui.items"), "alert"],
94
+ [window.LoadToAgentI18n.t("ui.helper_ai_history"), helperCount, window.LoadToAgentI18n.t("ui.items"), ""],
95
+ [window.LoadToAgentI18n.t("ui.tokens_used"), compact(totals.usage && totals.usage.total), window.LoadToAgentI18n.t("ui.items"), ""],
96
+ ];
97
+ $("#globalStats").innerHTML = items
98
+ .map(
99
+ ([label, value, unit, cls], index) => `<div class="global-stat ${cls}" data-motion-key="stat:${index}" data-motion-value="${esc(value)}">
100
+ <span>${label}</span>
101
+ <strong>${esc(value)}</strong>
102
+ <em>${unit}</em>
103
+ </div>`,
104
+ )
105
+ .join("");
106
+ $("#navAllCount").textContent = rootCount;
107
+ $("#navActiveCount").textContent = totals.active || 0;
108
+ $("#navWaitingCount").textContent = totals.waiting || 0;
109
+ $("#navTmuxCount").textContent = (state.snapshot && state.snapshot.tmux && state.snapshot.tmux.summary && state.snapshot.tmux.summary.aiPanes) || 0;
110
+ }
111
+
112
+ function formatBytes(value) {
113
+ const bytes = Math.max(0, Number(value || 0));
114
+ if (!bytes) return "0 B";
115
+ const units = ["B", "KB", "MB", "GB"];
116
+ const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
117
+ const amount = bytes / 1024 ** index;
118
+ return `${amount >= 10 || index === 0 ? amount.toFixed(0) : amount.toFixed(1)} ${units[index]}`;
119
+ }
120
+
121
+ function installationTypeLabel(value) {
122
+ const labels = {
123
+ desktop: window.LoadToAgentI18n.t("ui.desktop_installer"),
124
+ npm: window.LoadToAgentI18n.t("ui.global_npm_installation"),
125
+ source: window.LoadToAgentI18n.t("ui.local_development_build"),
126
+ };
127
+ return labels[value] || window.LoadToAgentI18n.t("ui.checking_installation_type");
128
+ }
129
+
130
+ function updatePresentation(update) {
131
+ const status = (update && update.status) || "idle";
132
+ const hasAsset = Boolean(update && update.asset);
133
+ const labels = {
134
+ idle: [
135
+ "·", window.LoadToAgentI18n.t("ui.version_status"),
136
+ window.LoadToAgentI18n.t("ui.ready_to_check_for_updates"), window.LoadToAgentI18n.t("ui.checks_the_latest_stable_github_release"),
137
+ ],
138
+ checking: [
139
+ "↻", window.LoadToAgentI18n.t("ui.checking_latest_version"),
140
+ window.LoadToAgentI18n.t("ui.checking_the_latest_version"), window.LoadToAgentI18n.t("ui.reading_the_latest_stable_github_release_tag"),
141
+ ],
142
+ current: [
143
+ "✓",
144
+ window.LoadToAgentI18n.t("ui.latest_version"),
145
+ window.LoadToAgentI18n.t("ui.you_are_up_to_date"),
146
+ window.LoadToAgentI18n.t("update.current_version", { version: (update && update.currentVersion) || "—" }),
147
+ ],
148
+ available: [
149
+ "↟",
150
+ window.LoadToAgentI18n.t("ui.update_available"),
151
+ window.LoadToAgentI18n.t("update.version_available", { version: (update && update.latestVersion) || "—" }),
152
+ hasAsset
153
+ ? window.LoadToAgentI18n.t("ui.a_verified_installer_for_this_computer_can_be_downloaded_in")
154
+ : window.LoadToAgentI18n.t("ui.the_release_exists_but_a_matching_installer_is_not_available"),
155
+ ],
156
+ downloading: [
157
+ "↓", window.LoadToAgentI18n.t("ui.downloading"), window.LoadToAgentI18n.t("ui.downloading_the_update_file"),
158
+ window.LoadToAgentI18n.t("ui.keep_the_app_open_until_the_download_finishes"),
159
+ ],
160
+ downloaded: [
161
+ "✓", window.LoadToAgentI18n.t("ui.ready_to_install"), window.LoadToAgentI18n.t("ui.the_update_file_is_ready"),
162
+ window.LoadToAgentI18n.t("ui.open_the_installer_and_follow_its_instructions_to_finish_updating"),
163
+ ],
164
+ error: [
165
+ "!", window.LoadToAgentI18n.t("ui.check_failed"), window.LoadToAgentI18n.t("ui.could_not_check_for_updates"),
166
+ window.LoadToAgentI18n.t("ui.check_your_internet_connection_and_try_again"),
167
+ ],
168
+ unsupported: [
169
+ "—", window.LoadToAgentI18n.t("ui.manual_update"), window.LoadToAgentI18n.t("ui.this_operating_system_requires_a_manual_update"),
170
+ window.LoadToAgentI18n.t("ui.get_the_latest_file_directly_from_github_releases"),
171
+ ],
172
+ };
173
+ return labels[status] || labels.idle;
174
+ }
175
+
176
+ function renderUpdateSettings() {
177
+ const update = state.update || { status: "idle", currentVersion: state.versions.app || "" };
178
+ const [glyph, label, title, text] = updatePresentation(update);
179
+ const available = ["available", "downloading", "downloaded"].includes(update.status);
180
+ const downloading = update.status === "downloading";
181
+ const downloaded = update.status === "downloaded";
182
+ const current = update.currentVersion || state.versions.app || "";
183
+ $("#sidebarAppVersion").textContent = current ? `v${current}` : "v—";
184
+ $("#updatePanel").dataset.updateStatus = update.status || "idle";
185
+ $("#currentVersion").textContent = current ? `v${current}` : "v—";
186
+ $("#latestVersion").textContent = update.latestVersion ? `v${update.latestVersion}` : window.LoadToAgentI18n.t("ui.not_checked");
187
+ $("#installationType").textContent = installationTypeLabel(update.installType);
188
+ $("#releasePublishedAt").textContent = update.publishedAt
189
+ ? window.LoadToAgentI18n.t("update.published", { date: new Date(update.publishedAt).toLocaleDateString(uiLocale()) })
190
+ : window.LoadToAgentI18n.t("ui.stable_releases_only");
191
+ $("#runtimeVersions").textContent = `Electron ${state.versions.electron || "—"} · Node ${state.versions.node || "—"}`;
192
+ $("#updateStateGlyph").textContent = glyph;
193
+ $("#updateStateLabel").textContent = label;
194
+ $("#updateStateTitle").textContent = title;
195
+ $("#updateStateText").textContent = text;
196
+ $("#checkUpdateBtn").disabled = update.status === "checking" || downloading;
197
+ $("#checkUpdateBtn").textContent =
198
+ update.status === "checking" ? window.LoadToAgentI18n.t("ui.checking") : window.LoadToAgentI18n.t("settings.update.check");
199
+ const install = $("#installUpdateBtn");
200
+ install.classList.toggle("hidden", !(available && (update.asset || downloaded)));
201
+ install.disabled = downloading;
202
+ install.textContent = downloading
203
+ ? window.LoadToAgentI18n.t("ui.downloading_2")
204
+ : downloaded
205
+ ? window.LoadToAgentI18n.t("ui.open_installer")
206
+ : window.LoadToAgentI18n.t("settings.update.download");
207
+ const progress = $("#updateProgress");
208
+ progress.classList.toggle("hidden", !downloading && !downloaded);
209
+ $("#updateProgressLabel").textContent = `${Math.max(0, Math.min(100, Number(update.progress || 0)))}%`;
210
+ $("#updateProgressBar").style.width = `${Math.max(0, Math.min(100, Number(update.progress || 0)))}%`;
211
+ $(".update-progress-track").setAttribute("aria-valuenow", String(Math.max(0, Math.min(100, Number(update.progress || 0)))));
212
+ $("#updateProgressBytes").textContent = downloaded
213
+ ? `${formatBytes(update.totalBytes || update.downloadedBytes)} · 파일 검증 완료`
214
+ : `${formatBytes(update.downloadedBytes)} / ${update.totalBytes ? formatBytes(update.totalBytes) : window.LoadToAgentI18n.t("ui.checking_size")}`;
215
+ const error = $("#updateError");
216
+ error.classList.toggle("hidden", !update.error);
217
+ error.textContent = update.error || "";
218
+ const notes = $("#releaseNotes");
219
+ notes.classList.toggle("hidden", !update.latestVersion);
220
+ $("#releaseNotesText").textContent =
221
+ (update.notes && update.notes.trim()) || window.LoadToAgentI18n.t("ui.no_release_notes_were_provided_for_this_release");
222
+ const notice = $("#updateNotice");
223
+ notice.classList.toggle("hidden", !available || state.view !== "all");
224
+ $("#updateNoticeTitle").textContent = window.LoadToAgentI18n.t("update.available_version", { version: update.latestVersion || "—" });
225
+ $("#updateNoticeText").textContent = downloaded
226
+ ? window.LoadToAgentI18n.t("ui.the_installer_is_ready")
227
+ : window.LoadToAgentI18n.t("ui.download_the_update_from_settings");
228
+ $("#navUpdateBadge").classList.toggle("hidden", !available);
229
+ }
230
+
231
+ function renderProviderOverview() {
232
+ pruneProviderFilters();
233
+ const summaries = (state.snapshot && state.snapshot.summary && state.snapshot.summary.providers) || state.providers;
234
+ const sessions = (state.snapshot && state.snapshot.sessions) || [];
235
+ $("#providerOverview").innerHTML = summaries
236
+ .map((provider) => {
237
+ const rootCount = sessions.filter((session) => session.provider === provider.id && !session.parentId).length;
238
+ const selected = state.providerFilters.has(provider.id);
239
+ return `<button type="button" class="provider-overview-card ${selected ? "selected" : ""}"
240
+ data-provider-card="${esc(provider.id)}"
241
+ data-motion-key="provider:${esc(provider.id)}"
242
+ data-motion-value="${provider.active || 0}:${rootCount}:${(provider.usage && provider.usage.total) || 0}"
243
+ style="${providerStyle(provider.id)}"
244
+ aria-pressed="${selected ? "true" : "false"}">
245
+ <div class="poc-head">
246
+ <span class="provider-mark">${esc(provider.mark)}</span>
247
+ <div><strong>${esc(provider.label)}</strong><small>${esc(provider.company)}</small></div>
248
+ <span class="poc-state ${provider.installed ? "online" : ""}">
249
+ ${provider.installed ? window.LoadToAgentI18n.t("ui.available") : window.LoadToAgentI18n.t("ui.setup_required")}
250
+ </span>
251
+ </div>
252
+ <div class="poc-metrics">
253
+ <div><b>${provider.active || 0}</b><span>${window.LoadToAgentI18n.t("ui.active_ai")}</span></div>
254
+ <div><b>${rootCount}</b><span>${window.LoadToAgentI18n.t("ui.main_tasks")}</span></div>
255
+ <div><b>${compact(provider.usage && provider.usage.total)}</b><span>${window.LoadToAgentI18n.t("ui.tokens_used_2")}</span></div>
256
+ </div>
257
+ </button>`;
258
+ })
259
+ .join("");
260
+ }
261
+
262
+ function pruneProviderFilters() {
263
+ const valid = new Set(state.providers.map((provider) => provider.id));
264
+ for (const id of [...state.providerFilters]) if (!valid.has(id)) state.providerFilters.delete(id);
265
+ if (valid.size > 0 && state.providerFilters.size === valid.size) state.providerFilters.clear();
266
+ }
267
+
268
+ function toggleProviderFilter(providerId) {
269
+ pruneProviderFilters();
270
+ if (providerId === "all") state.providerFilters.clear();
271
+ else if (state.providerFilters.has(providerId)) state.providerFilters.delete(providerId);
272
+ else state.providerFilters.add(providerId);
273
+ if (state.providers.length > 0 && state.providerFilters.size === state.providers.length) state.providerFilters.clear();
274
+ }
275
+
276
+ function renderProviderFilter() {
277
+ pruneProviderFilters();
278
+ const allSelected = state.providerFilters.size === 0;
279
+ const button = (id, label, mark = "") => {
280
+ const selected = id === "all" ? allSelected : state.providerFilters.has(id);
281
+ return `<button type="button" class="provider-filter-chip ${selected ? "selected" : ""}"
282
+ data-provider-filter="${esc(id)}" aria-pressed="${selected ? "true" : "false"}">
283
+ ${mark ? `<span aria-hidden="true">${esc(mark)}</span>` : ""}<b>${esc(label)}</b>
284
+ </button>`;
285
+ };
286
+ $("#providerFilter").innerHTML =
287
+ button("all", window.LoadToAgentI18n.t("ui.all_ai")) +
288
+ state.providers.map((provider) => button(provider.id, provider.label, provider.mark)).join("");
289
+ }
290
+
291
+ function announceProviderFilter() {
292
+ const labels = state.providerFilters.size
293
+ ? state.providers.filter((provider) => state.providerFilters.has(provider.id)).map((provider) => provider.label).join(", ")
294
+ : window.LoadToAgentI18n.t("ui.all_ai");
295
+ $("#providerFilterStatus").textContent = window.LoadToAgentI18n.t("filter.result_summary", {
296
+ providers: labels,
297
+ count: filteredSessions().length,
298
+ });
299
+ }
300
+
301
+ function filteredSessions() {
302
+ const allSessions = [...((state.snapshot && state.snapshot.sessions) || [])];
303
+ let sessions = state.view === "waiting" ? allSessions : allSessions.filter((session) => !session.parentId);
304
+ if (state.view === "active") sessions = sessions.filter((session) => session.status === "running" || session.status === "starting");
305
+ if (state.view === "waiting") sessions = sessions.filter((session) => session.status === "waiting");
306
+ if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
307
+ sessions = sessions.filter(matchesWorkspaceFilter);
308
+ const query = state.search.trim().toLowerCase();
309
+ if (query) {
310
+ sessions = sessions.filter((session) =>
311
+ [session.title, session.model, session.cwd, session.agentName, ...(session.messages || []).slice(-12).map((item) => item.text)]
312
+ .join(" ")
313
+ .toLowerCase()
314
+ .includes(query),
315
+ );
316
+ }
317
+ if (state.sort === "tokens") sessions.sort((a, b) => Number((b.usage && b.usage.total) || 0) - Number((a.usage && a.usage.total) || 0));
318
+ else if (state.sort === "context") sessions.sort((a, b) => Number((b.context && b.context.percent) || 0) - Number((a.context && a.context.percent) || 0));
319
+ else
320
+ sessions.sort((a, b) => {
321
+ const activeA = a.status === "running" || a.status === "starting" ? 1 : 0;
322
+ const activeB = b.status === "running" || b.status === "starting" ? 1 : 0;
323
+ return activeB - activeA || Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
324
+ });
325
+ return sessions;
326
+ }
327
+
328
+ function graphFilteredSessions() {
329
+ let sessions = [...((state.snapshot && state.snapshot.sessions) || [])];
330
+ if (state.providerFilters.size) sessions = sessions.filter((session) => state.providerFilters.has(session.provider));
331
+ sessions = sessions.filter(matchesWorkspaceFilter);
332
+ const query = state.search.trim().toLowerCase();
333
+ if (query)
334
+ sessions = sessions.filter((session) =>
335
+ [session.title, session.model, session.cwd, session.agentName, session.agentRole, ...(session.messages || []).map((item) => item.text)]
336
+ .join(" ")
337
+ .toLowerCase()
338
+ .includes(query),
339
+ );
340
+ return sessions;
341
+ }
342
+
343
+ return {
344
+ renderProviderRail,
345
+ isProjectlessSession,
346
+ sessionWorkspaceLabel,
347
+ matchesWorkspaceFilter,
348
+ renderWorkspaces,
349
+ renderGlobalStats,
350
+ formatBytes,
351
+ installationTypeLabel,
352
+ updatePresentation,
353
+ renderUpdateSettings,
354
+ renderProviderOverview,
355
+ renderProviderFilter,
356
+ toggleProviderFilter,
357
+ announceProviderFilter,
358
+ filteredSessions,
359
+ graphFilteredSessions,
360
+ };
361
+ };
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+
3
+ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
+
5
+ window.LoadToAgentAppFactories.createDrawerContent = function createDrawerContent(context = {}) {
6
+ const { esc, uiLocale, state, messageContentHtml, compact, fullNumber, timeOnly, providerInfo, statusIcon, agentPathTaskName, snapshotSession } = context;
7
+
8
+ function chatHtml(session) {
9
+ const messages = session.messages || [];
10
+ if (!messages.length) return '<div class="empty-state"><h3>표시할 대화가 없습니다</h3></div>';
11
+ const conversation = messages.filter((message) => message.role === "user" || message.role === "assistant");
12
+ const activities = messages.filter((message) => message.role !== "user" && message.role !== "assistant");
13
+ const omitted = Number(session.omittedMessages || 0);
14
+ const notice =
15
+ omitted || session.truncated
16
+ ? `<div class="chat-truncated">이 작업의 최근 기록을 표시합니다${omitted ? ` · 이전 ${omitted.toLocaleString(uiLocale())}개 메시지 생략` : ""}</div>`
17
+ : "";
18
+ const statusLabels = {
19
+ started: "실행 중", running: "실행 중",
20
+ done: window.LoadToAgentI18n.t("ui.completed"), completed: window.LoadToAgentI18n.t("ui.completed"), failed: "실패",
21
+ };
22
+ const statusLabel = (value) => statusLabels[value] || value || "";
23
+ const rows = conversation
24
+ .map((message) => {
25
+ const role = message.role === "assistant" ? "assistant" : message.role === "tool" ? "tool" : message.role === "system" ? "system" : "user";
26
+ const label =
27
+ role === "assistant"
28
+ ? providerInfo(session.provider).label
29
+ : role === "tool"
30
+ ? message.title || "도구"
31
+ : message.role === "system"
32
+ ? "시스템"
33
+ : "사용자";
34
+ const avatar = role === "assistant" ? providerInfo(session.provider).mark : role === "tool" ? "⌘" : role === "system" ? "i" : "ME";
35
+ const fullTime = new Date(message.timestamp).toLocaleString(uiLocale());
36
+ return `<div class="chat-row ${role}" data-message-id="${esc(message.id || "")}">
37
+ <span class="chat-avatar">${esc(avatar)}</span>
38
+ <div class="chat-bubble">
39
+ <div class="chat-bubble-head">
40
+ <b>${esc(label)}</b>
41
+ <span title="${esc(fullTime)}">${esc(timeOnly(message.timestamp))}</span>
42
+ ${message.status ? `<span>${esc(statusLabel(message.status))}</span>` : ""}
43
+ </div>${messageContentHtml(message)}</div>
44
+ </div>`;
45
+ })
46
+ .join("");
47
+ const activityHtml = activities.length
48
+ ? `<details class="chat-activities">
49
+ <summary>도구·시스템 활동 ${activities.length}건 보기</summary>
50
+ <div>${activities
51
+ .map(
52
+ (message) => `<article>
53
+ <header>
54
+ <b>${esc(message.title || (message.role === "tool" ? "도구 실행" : "시스템"))}</b>
55
+ <span>${esc(statusLabel(message.status))} · ${esc(timeOnly(message.timestamp))}</span>
56
+ </header>${messageContentHtml(message)}</article>`,
57
+ )
58
+ .join("")}</div>
59
+ </details>`
60
+ : "";
61
+ const emptyConversation = conversation.length ? "" : '<div class="empty-state compact"><h3>사용자와 AI의 대화는 아직 없습니다</h3></div>';
62
+ return `${notice}<div class="chat-history-head">
63
+ <span>대화 ${conversation.length}개${activities.length ? ` · 활동 ${activities.length}건` : ""}</span>
64
+ <button type="button" data-scroll-latest>가장 최근 대화 ↓</button>
65
+ </div>
66
+ <div class="chat-list">${rows}${emptyConversation}${activityHtml}<div class="chat-latest-anchor" aria-label="가장 최근 대화">
67
+ </div>
68
+ </div>`;
69
+ }
70
+
71
+ function lifecycleHtml(session) {
72
+ const events = session.lifecycle || [];
73
+ if (!events.length) return '<div class="empty-state"><h3>아직 기록된 진행 과정이 없습니다</h3></div>';
74
+ return `<div class="lifecycle-list">${events
75
+ .map(
76
+ (event) => `<div class="lifecycle-event ${esc(event.status)}">
77
+ <span class="life-node">${statusIcon(event.type)}</span>
78
+ <div class="life-copy">
79
+ <b>${esc(event.label)}</b>
80
+ <span>${esc(event.detail || event.type)}</span>
81
+ </div>
82
+ <time>${esc(timeOnly(event.timestamp))}</time>
83
+ </div>`,
84
+ )
85
+ .join("")}</div>`;
86
+ }
87
+
88
+ function tokensHtml(session) {
89
+ const usage = session.usage || {};
90
+ const turn = session.turnUsage || {};
91
+ const context = session.context || {};
92
+ const sourceLabel =
93
+ context.source === "session"
94
+ ? "이 작업 기록에서 직접 확인한 기억 공간"
95
+ : context.source === "model-catalog"
96
+ ? "AI 모델 정보에 적힌 기억 공간"
97
+ : "기억 공간 크기 정보 없음";
98
+ return `<div class="token-hero" style="--drawer-provider:${providerInfo(session.provider).accent}">
99
+ <div class="token-hero-head">
100
+ <span>AI의 기억 공간 사용량</span>
101
+ <b>${context.window ? `${fullNumber(context.used)} / ${fullNumber(context.window)} 토큰` : `${fullNumber(context.used)} 토큰`}</b>
102
+ </div>
103
+ <div class="big-context"><span style="width:${Math.min(100, context.percent || 0)}%"></span></div>
104
+ <div class="context-scale">
105
+ <span>0</span><span>${(context.percent || 0).toFixed(1)}%</span>
106
+ <span>${context.window ? compact(context.window) : "--"}</span>
107
+ </div>
108
+ </div>
109
+ <div class="token-grid">
110
+ <div class="token-tile"><span>AI가 받은 글</span><strong>${fullNumber(usage.input)}</strong><small>AI에게 전달된 내용의 양</small></div>
111
+ <div class="token-tile"><span>AI가 쓴 글</span><strong>${fullNumber(usage.output)}</strong><small>AI가 답하고 만든 내용의 양</small></div>
112
+ <div class="token-tile"><span>다시 사용한 기억</span><strong>${fullNumber(usage.cachedInput)}</strong><small>전에 읽은 내용을 다시 활용한 양</small></div>
113
+ <div class="token-tile"><span>새로 저장한 기억</span><strong>${fullNumber(usage.cacheWrite)}</strong><small>다음에 다시 쓰도록 저장한 양</small></div>
114
+ <div class="token-tile"><span>생각에 사용</span><strong>${fullNumber(usage.reasoning)}</strong><small>AI가 따로 알려준 경우만 표시</small></div>
115
+ <div class="token-tile"><span>전체 사용량</span><strong>${fullNumber(usage.total)}</strong><small>이 작업에서 사용한 토큰 합계</small></div>
116
+ <div class="token-tile"><span>최근에 받은 글</span><strong>${fullNumber(turn.input)}</strong><small>가장 최근 대화 기준</small></div>
117
+ <div class="token-tile"><span>최근 대화 전체</span><strong>${fullNumber(turn.total)}</strong><small>가장 최근 한 번의 사용량</small></div>
118
+ </div><div class="token-note">${esc(sourceLabel)}입니다. 토큰은 AI가 글을 읽고 쓰는 양을 세는 단위이고, 기억 공간은 AI가 한 번에 기억할 수 있는 양입니다.</div>`;
119
+ }
120
+
121
+ function subagentCommunicationEvents(session) {
122
+ if (!session || !session.parentId) return [];
123
+ const parent = state.details.get(session.parentId) || snapshotSession(session.parentId);
124
+ const all = (parent && parent.collaboration && parent.collaboration.communications) || [];
125
+ const taskName = session.taskName || (session.delegation && session.delegation.taskName) || agentPathTaskName(session.agentPath);
126
+ return all
127
+ .filter((event) => ["assignment", "started", "followup", "message", "result", "interrupt"].includes(event.kind))
128
+ .filter((event) => event.childId === session.id || (taskName && event.taskName === taskName));
129
+ }
130
+
131
+ function subagentTextPreview(value, maxCharacters = 360) {
132
+ const text = String(value || "")
133
+ .replace(/\s+/g, " ")
134
+ .trim();
135
+ if (text.length <= maxCharacters) return { text, truncated: false };
136
+ return { text: `${text.slice(0, maxCharacters).trimEnd()}…`, truncated: true };
137
+ }
138
+
139
+ function protectedSubagentEventText(event, taskName) {
140
+ if (event.kind === "followup") return "보호된 추가 작업 지시를 전달했습니다.";
141
+ if (event.kind === "message") return "보호된 메시지를 전달했습니다.";
142
+ return `${taskName || "이 작업"}을 서브에이전트에게 배정했습니다.`;
143
+ }
144
+
145
+ function subagentConversationHtml(session) {
146
+ const events = subagentCommunicationEvents(session);
147
+ const taskName = session.taskName || (session.delegation && session.delegation.taskName) || session.title;
148
+ const childPath = String(session.agentPath || "");
149
+ const endpointIsChild = (value) => {
150
+ const endpoint = String(value || "");
151
+ return endpoint === childPath || endpoint === session.id || agentPathTaskName(endpoint) === taskName;
152
+ };
153
+ const enriched = events.map((event) => ({ ...event, fromChild: event.kind === "result" || endpointIsChild(event.from) }));
154
+ const received = enriched.filter((event) => !event.fromChild && event.kind !== "started").length;
155
+ const answered = enriched.filter((event) => event.fromChild).length;
156
+ if (!events.length)
157
+ return '<div class="empty-state"><h3>메인 AI와 주고받은 기록이 없습니다</h3><p>세션 로그에서 배정·추가 지시·결과 반환 이벤트를 찾지 못했습니다.</p></div>';
158
+ const provider = providerInfo(session.provider);
159
+ const rows = enriched
160
+ .map((event) => {
161
+ const runtime = event.kind === "started";
162
+ const role = runtime ? "system" : event.fromChild ? "assistant" : "user";
163
+ const label = runtime ? "실행 상태" : event.fromChild ? `서브 AI · ${session.agentName || taskName}` : "메인 AI";
164
+ const avatar = runtime ? "↗" : event.fromChild ? provider.mark : "M";
165
+ const route = runtime ? "런타임 → 서브 AI" : event.fromChild ? "서브 AI → 메인 AI" : "메인 AI → 서브 AI";
166
+ const text = event.protected
167
+ ? protectedSubagentEventText(event, taskName)
168
+ : event.text || (runtime ? "서브에이전트 실행이 시작되었습니다." : "내용 없이 통신 상태만 기록되었습니다.");
169
+ const preview = subagentTextPreview(text);
170
+ const eventLabel = `${event.label || event.kind}${event.assignmentSource === "parent-narration" ? " · 작업 시작 직전 메인 AI 설명" : ""}`;
171
+ return `<div class="chat-row ${role} subagent-dialog-row" data-subagent-communication="${esc(event.kind)}">
172
+ <span class="chat-avatar">${esc(avatar)}</span>
173
+ <div class="chat-bubble">
174
+ <div class="chat-bubble-head">
175
+ <b>${esc(label)}</b><span class="subagent-route">${esc(route)}</span>
176
+ <span>${esc(timeOnly(event.timestamp))}</span>
177
+ </div>
178
+ <small class="subagent-event-label">${esc(eventLabel)}</small>
179
+ <div class="chat-content subagent-message-preview${preview.truncated ? " is-truncated" : ""}"
180
+ data-subagent-message-preview data-truncated="${preview.truncated ? "true" : "false"}">
181
+ <p>${esc(preview.text)}</p>
182
+ </div>
183
+ </div>
184
+ </div>`;
185
+ })
186
+ .join("");
187
+ return `<div class="subagent-conversation-summary" data-subagent-dialog-count="${events.length}">
188
+ <span>
189
+ <small>받은 지시·응답</small>
190
+ <b>${received}</b>건</span>
191
+ <span>
192
+ <small>메인에 보낸 답변</small>
193
+ <b>${answered}</b>건</span>
194
+ <span>
195
+ <small>전체 소통</small>
196
+ <b>${events.length}</b>건</span>
197
+ </div>
198
+ <div class="chat-history-head"><span><b>${esc(taskName)}</b>와 메인 AI가 주고받은 내용만 표시</span><button type="button" data-scroll-latest>가장 최근 대화 ↓</button></div>
199
+ <div class="chat-list subagent-dialog-list">${rows}<div class="chat-latest-anchor" aria-label="가장 최근 대화"></div></div>`;
200
+ }
201
+
202
+ return { chatHtml, lifecycleHtml, tokensHtml, subagentCommunicationEvents, subagentTextPreview, protectedSubagentEventText, subagentConversationHtml };
203
+ };
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+
3
+ window.LoadToAgentAppFactories = window.LoadToAgentAppFactories || {};
4
+
5
+ window.LoadToAgentAppFactories.createDrawerData = function createDrawerData(context = {}) {
6
+ const { reportRecoverableError, state } = context;
7
+
8
+ async function loadSessionDetail(id, force = false) {
9
+ if (!force && state.details.has(id)) return state.details.get(id);
10
+ state.detailErrors.delete(id);
11
+ state.detailLoadingIds.add(id);
12
+ context.renderDrawer();
13
+ try {
14
+ const detail = await window.loadtoagent.sessionDetail(id);
15
+ if (detail) state.details.set(id, detail);
16
+ return detail;
17
+ } catch (error) {
18
+ state.detailErrors.set(id, (error && error.message) || "작업 기록을 불러오지 못했습니다.");
19
+ return null;
20
+ } finally {
21
+ state.detailLoadingIds.delete(id);
22
+ if (state.selectedId === id) {
23
+ state.drawerForceLatest = state.drawerTab === "chat";
24
+ context.renderDrawer();
25
+ }
26
+ }
27
+ }
28
+
29
+ async function loadSubagentParentDetail(child) {
30
+ if (!child || !child.parentId || state.details.has(child.parentId)) return;
31
+ try {
32
+ const detail = await window.loadtoagent.sessionDetail(child.parentId);
33
+ if (detail) state.details.set(child.parentId, detail);
34
+ if (state.drawerMode === "subagent" && state.selectedId === child.id) context.renderDrawer();
35
+ } catch (error) {
36
+ reportRecoverableError("subagent-parent-detail", error);
37
+ }
38
+ }
39
+
40
+ return { loadSessionDetail, loadSubagentParentDetail };
41
+ };